From b00bba23c95a59b317776b233cb7971c83e0be7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 29 Jan 2024 10:02:24 +0100 Subject: [PATCH 1/4] Add path, kind and lang to content front matter Note that none of these can be set via cascade (you will get an error) Fixes #11544 --- go.mod | 2 +- go.sum | 2 + hugolib/content_map.go | 6 +- hugolib/content_map_page.go | 89 +++-- hugolib/hugo_sites.go | 3 + hugolib/integrationtest_builder.go | 9 + hugolib/page.go | 33 +- hugolib/page__common.go | 3 - hugolib/page__content.go | 180 ++++++--- hugolib/page__meta.go | 352 +++++++++--------- hugolib/page__new.go | 112 ++++-- hugolib/page__paths.go | 2 +- hugolib/page__per_output.go | 27 +- hugolib/params_test.go | 109 +++++- hugolib/shortcode.go | 4 +- hugolib/site_new.go | 2 + hugolib/site_render.go | 2 +- resources/page/page_matcher.go | 13 + resources/page/pagemeta/page_frontmatter.go | 101 +++-- .../page/pagemeta/page_frontmatter_test.go | 37 +- resources/page/pagemeta/pagemeta.go | 7 - resources/resource/dates.go | 41 +- 22 files changed, 707 insertions(+), 429 deletions(-) diff --git a/go.mod b/go.mod index e8beedbfb56..c74972d0bb8 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/marekm4/color-extractor v1.2.1 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/hashstructure v1.1.0 - github.com/mitchellh/mapstructure v1.5.0 + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c github.com/muesli/smartcrop v0.3.0 github.com/niklasfasching/go-org v1.7.0 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 3c1cfe7ff3b..4795d0d688a 100644 --- a/go.sum +++ b/go.sum @@ -359,6 +359,8 @@ github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9km github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= diff --git a/hugolib/content_map.go b/hugolib/content_map.go index fefa90bf152..96013c4ed8c 100644 --- a/hugolib/content_map.go +++ b/hugolib/content_map.go @@ -187,7 +187,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { if pi.IsContent() { // Create the page now as we need it at assemembly time. // The other resources are created if needed. - pageResource, err := m.s.h.newPage( + pageResource, pi, err := m.s.h.newPage( &pageMeta{ f: source.NewFileInfo(fim), pathInfo: pi, @@ -197,6 +197,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { if err != nil { return err } + key = pi.Base() + rs = &resourceSource{r: pageResource} } else { rs = &resourceSource{path: pi, opener: r, fi: fim} @@ -226,7 +228,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { }, )) // A content file. - p, err := m.s.h.newPage( + p, pi, err := m.s.h.newPage( &pageMeta{ f: source.NewFileInfo(fi), pathInfo: pi, diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 536f23ccd3f..9fee7400330 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -43,6 +43,7 @@ import ( "github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/resource" ) @@ -97,7 +98,6 @@ type pageMap struct { cacheContentRendered *dynacache.Partition[string, *resources.StaleValue[contentSummary]] cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]] contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]] - cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]] cfg contentMapConfig } @@ -147,7 +147,6 @@ func (t *pageTrees) collectIdentities(key string) []identity.Identity { // collectIdentitiesSurrounding collects all identities surrounding the given key. func (t *pageTrees) collectIdentitiesSurrounding(key string, maxSamplesPerTree int) []identity.Identity { - // TODO1 test language coverage from this. ids := t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treePages) ids = append(ids, t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treeResources)...) return ids @@ -483,7 +482,7 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources return nil, err } - if translationKey := ps.m.translationKey; translationKey != "" { + if translationKey := ps.m.pageConfig.TranslationKey; translationKey != "" { // This this should not be a very common case. // Merge in resources from the other languages. translatedPages, _ := m.s.h.translationKeyPages.Get(translationKey) @@ -539,9 +538,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources sort.SliceStable(res, lessFunc) - if len(ps.m.resourcesMetadata) > 0 { + if len(ps.m.pageConfig.Resources) > 0 { for i, r := range res { - res[i] = resources.CloneWithMetadataIfNeeded(ps.m.resourcesMetadata, r) + res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.Resources, r) } sort.SliceStable(res, lessFunc) } @@ -819,7 +818,6 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) * cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), - cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](mcache, fmt.Sprintf("/cont/src/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), cfg: contentMapConfig{ lang: s.Lang(), @@ -1215,7 +1213,7 @@ func (sa *sitePagesAssembler) applyAggregates() error { // Home page gets it's cascade from the site config. cascade = sa.conf.Cascade.Config - if pageBundle.m.cascade == nil { + if pageBundle.m.pageConfig.Cascade == nil { // Pass the site cascade downwards. pw.WalkContext.Data().Insert(keyPage, cascade) } @@ -1227,12 +1225,12 @@ func (sa *sitePagesAssembler) applyAggregates() error { } if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 { - oldDates := pageBundle.m.dates + oldDates := pageBundle.m.pageConfig.Dates // We need to wait until after the walk to determine if any of the dates have changed. pw.WalkContext.AddPostHook( func() error { - if oldDates != pageBundle.m.dates { + if oldDates != pageBundle.m.pageConfig.Dates { sa.assembleChanges.Add(pageBundle) } return nil @@ -1251,11 +1249,12 @@ func (sa *sitePagesAssembler) applyAggregates() error { const eventName = "dates" if n.isContentNodeBranch() { - if pageBundle.m.cascade != nil { + if pageBundle.m.pageConfig.Cascade != nil { // Pass it down. - pw.WalkContext.Data().Insert(keyPage, pageBundle.m.cascade) + pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.Cascade) } - wasZeroDates := resource.IsZeroDates(pageBundle.m.dates) + + wasZeroDates := pageBundle.m.pageConfig.Dates.IsAllDatesZero() if wasZeroDates || pageBundle.IsHome() { pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNodeI]) { sp, ok := e.Source.(*pageState) @@ -1264,15 +1263,15 @@ func (sa *sitePagesAssembler) applyAggregates() error { } if wasZeroDates { - pageBundle.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates) + pageBundle.m.pageConfig.Dates.UpdateDateAndLastmodIfAfter(sp.m.pageConfig.Dates) } if pageBundle.IsHome() { - if pageBundle.m.dates.Lastmod().After(pageBundle.s.lastmod) { - pageBundle.s.lastmod = pageBundle.m.dates.Lastmod() + if pageBundle.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) { + pageBundle.s.lastmod = pageBundle.m.pageConfig.Dates.Lastmod } - if sp.m.dates.Lastmod().After(pageBundle.s.lastmod) { - pageBundle.s.lastmod = sp.m.dates.Lastmod() + if sp.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) { + pageBundle.s.lastmod = sp.m.pageConfig.Dates.Lastmod } } }) @@ -1351,9 +1350,9 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error { p := n.(*pageState) if p.Kind() != kinds.KindTerm { // The other kinds were handled in applyAggregates. - if p.m.cascade != nil { + if p.m.pageConfig.Cascade != nil { // Pass it down. - pw.WalkContext.Data().Insert(s, p.m.cascade) + pw.WalkContext.Data().Insert(s, p.m.pageConfig.Cascade) } } @@ -1388,14 +1387,14 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error { // Send the date info up the tree. pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: n, Path: s, Name: eventName}) - if resource.IsZeroDates(p.m.dates) { + if p.m.pageConfig.Dates.IsAllDatesZero() { pw.WalkContext.AddEventListener(eventName, s, func(e *doctree.Event[contentNodeI]) { sp, ok := e.Source.(*pageState) if !ok { return } - p.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates) + p.m.pageConfig.Dates.UpdateDateAndLastmodIfAfter(sp.m.pageConfig.Dates) }) } @@ -1443,8 +1442,8 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error { // This is a little out of place, but is conveniently put here. // Check if translationKey is set by user. // This is to support the manual way of setting the translationKey in front matter. - if ps.m.translationKey != "" { - sa.s.h.translationKeyPages.Append(ps.m.translationKey, ps) + if ps.m.pageConfig.TranslationKey != "" { + sa.s.h.translationKeyPages.Append(ps.m.pageConfig.TranslationKey, ps) } if sa.pageMap.cfg.taxonomyTermDisabled { @@ -1477,9 +1476,13 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error { singular: viewName.singular, s: sa.Site, pathInfo: pi, - kind: kinds.KindTerm, + pageMetaParams: pageMetaParams{ + pageConfig: &pagemeta.PageConfig{ + Kind: kinds.KindTerm, + }, + }, } - n, err := sa.h.newPage(m) + n, pi, err := sa.h.newPage(m) if err != nil { return false, err } @@ -1524,7 +1527,7 @@ func (sa *sitePagesAssembler) assembleResources() error { targetPaths := ps.targetPaths() baseTarget := targetPaths.SubResourceBaseTarget duplicateResourceFiles := true - if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.markup) { + if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.pageConfig.Markup) { duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles } @@ -1566,7 +1569,7 @@ func (sa *sitePagesAssembler) assembleResources() error { BasePathTargetPath: baseTarget, Name: relPath, NameOriginal: relPathOriginal, - LazyPublish: !ps.m.buildConfig.PublishResources, + LazyPublish: !ps.m.pageConfig.Build.PublishResources, } r, err := ps.m.s.ResourceSpec.NewResource(rd) if err != nil { @@ -1631,7 +1634,7 @@ func (sa *sitePagesAssembler) removeShouldNotBuild() error { case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy: // We need to keep these for the structure, but disable // them so they don't get listed/rendered. - (&p.m.buildConfig).Disable() + (&p.m.pageConfig.Build).Disable() default: keys = append(keys, key) } @@ -1673,13 +1676,17 @@ func (sa *sitePagesAssembler) addStandalonePages() error { } m := &pageMeta{ - s: s, - pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix), - kind: kind, + s: s, + pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix), + pageMetaParams: pageMetaParams{ + pageConfig: &pagemeta.PageConfig{ + Kind: kind, + }, + }, standaloneOutputFormat: f, } - p, _ := s.h.newPage(m) + p, _, _ := s.h.newPage(m) tree.InsertIntoValuesDimension(key, p) } @@ -1756,7 +1763,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error { pathInfo: pth, } - ps, err := sa.h.newPage(m) + ps, pth, err := sa.h.newPage(m) if err != nil { return false, err } @@ -1781,9 +1788,13 @@ func (sa *sitePagesAssembler) addMissingRootSections() error { m := &pageMeta{ s: sa.Site, pathInfo: p, - kind: kinds.KindHome, + pageMetaParams: pageMetaParams{ + pageConfig: &pagemeta.PageConfig{ + Kind: kinds.KindHome, + }, + }, } - n, err := sa.h.newPage(m) + n, p, err := sa.h.newPage(m) if err != nil { return err } @@ -1810,10 +1821,14 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error { m := &pageMeta{ s: sa.Site, pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"), - kind: kinds.KindTaxonomy, + pageMetaParams: pageMetaParams{ + pageConfig: &pagemeta.PageConfig{ + Kind: kinds.KindTaxonomy, + }, + }, singular: viewName.singular, } - p, _ := sa.h.newPage(m) + p, _, _ := sa.h.newPage(m) tree.InsertIntoValuesDimension(key, p) } } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 80e75445374..1b284061734 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -26,6 +26,7 @@ import ( "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/hugolib/doctree" + "github.com/gohugoio/hugo/resources" "github.com/fsnotify/fsnotify" @@ -72,6 +73,8 @@ type HugoSites struct { // Cache for page listings. cachePages *dynacache.Partition[string, page.Pages] + // Cache for content sources. + cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]] // Before Hugo 0.122.0 we managed all translations in a map using a translationKey // that could be overridden in front matter. diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 3fec04df0f7..a46ae72751c 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -80,6 +80,15 @@ func Test(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder { return NewIntegrationTestBuilder(cfg).Build() } +// TestE is the same as Test, but returns an error instead of failing the test. +func TestE(t testing.TB, files string, opts ...TestOpt) (*IntegrationTestBuilder, error) { + cfg := IntegrationTestConfig{T: t, TxtarString: files} + for _, o := range opts { + o(&cfg) + } + return NewIntegrationTestBuilder(cfg).BuildE() +} + // TestRunning is a convenience method to create a new IntegrationTestBuilder from some files with Running set to true and run a build. // Deprecated: Use Test with TestOptRunning instead. func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder { diff --git a/hugolib/page.go b/hugolib/page.go index f8ec5e22578..822b7c021f4 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import ( "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/related" + "github.com/spf13/afero" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/tableofcontents" @@ -197,7 +198,7 @@ func (p *pageHeadingsFiltered) page() page.Page { // For internal use by the related content feature. func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { - r, err := p.content.contentToC(ctx, p.pageOutput.pco) + r, err := p.m.content.contentToC(ctx, p.pageOutput.pco) if err != nil { panic(err) } @@ -313,14 +314,14 @@ func (p *pageState) Pages() page.Pages { // RawContent returns the un-rendered source content without // any leading front matter. func (p *pageState) RawContent() string { - if p.content.parseInfo.itemsStep2 == nil { + if p.m.content.pi.itemsStep2 == nil { return "" } - start := p.content.parseInfo.posMainContent + start := p.m.content.pi.posMainContent if start == -1 { start = 0 } - source, err := p.content.contentSource() + source, err := p.m.content.pi.contentSource(p.m.content) if err != nil { panic(err) } @@ -332,11 +333,11 @@ func (p *pageState) Resources() resource.Resources { } func (p *pageState) HasShortcode(name string) bool { - if p.content.shortcodeState == nil { + if p.m.content.shortcodeState == nil { return false } - return p.content.shortcodeState.hasName(name) + return p.m.content.shortcodeState.hasName(name) } func (p *pageState) Site() page.Site { @@ -355,8 +356,8 @@ func (p *pageState) IsTranslated() bool { // TranslationKey returns the key used to identify a translation of this content. func (p *pageState) TranslationKey() string { - if p.m.translationKey != "" { - return p.m.translationKey + if p.m.pageConfig.TranslationKey != "" { + return p.m.pageConfig.TranslationKey } return p.Path() } @@ -365,9 +366,9 @@ func (p *pageState) TranslationKey() string { func (p *pageState) AllTranslations() page.Pages { key := p.Path() + "/" + "translations-all" pages, err := p.s.pageMap.getOrCreatePagesFromCache(key, func(string) (page.Pages, error) { - if p.m.translationKey != "" { + if p.m.pageConfig.TranslationKey != "" { // translationKey set by user. - pas, _ := p.s.h.translationKeyPages.Get(p.m.translationKey) + pas, _ := p.s.h.translationKeyPages.Get(p.m.pageConfig.TranslationKey) pasc := make(page.Pages, len(pas)) copy(pasc, pas) page.SortByLanguage(pasc) @@ -534,7 +535,7 @@ var defaultRenderStringOpts = renderStringOpts{ Markup: "", // Will inherit the page's value when not set. } -func (p *pageMeta) wrapError(err error) error { +func (p *pageMeta) wrapError(err error, sourceFs afero.Fs) error { if err == nil { panic("wrapError with nil") } @@ -544,18 +545,18 @@ func (p *pageMeta) wrapError(err error) error { return fmt.Errorf("%q: %w", p.Path(), err) } - return hugofs.AddFileInfoToError(err, p.File().FileInfo(), p.s.SourceSpec.Fs.Source) + return hugofs.AddFileInfoToError(err, p.File().FileInfo(), sourceFs) } // wrapError adds some more context to the given error if possible/needed func (p *pageState) wrapError(err error) error { - return p.m.wrapError(err) + return p.m.wrapError(err, p.s.h.SourceFs) } func (p *pageState) getContentConverter() converter.Converter { var err error p.contentConverterInit.Do(func() { - markup := p.m.markup + markup := p.m.pageConfig.Markup if markup == "html" { // Only used for shortcode inner content. markup = "markdown" @@ -612,7 +613,7 @@ func (p *pageState) posFromInput(input []byte, offset int) text.Position { } func (p *pageState) posOffset(offset int) text.Position { - return p.posFromInput(p.content.mustSource(), offset) + return p.posFromInput(p.m.content.mustSource(), offset) } // shiftToOutputFormat is serialized. The output format idx refers to the diff --git a/hugolib/page__common.go b/hugolib/page__common.go index 0881affe7be..164776842e7 100644 --- a/hugolib/page__common.go +++ b/hugolib/page__common.go @@ -91,9 +91,6 @@ type pageCommon struct { layoutDescriptor layouts.LayoutDescriptor layoutDescriptorInit sync.Once - // The source and the parsed page content. - content *cachedContent - // Set if feature enabled and this is in a Git repo. gitInfo source.GitInfo codeowners []string diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 64ce83f0ebf..62e78c61271 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -20,6 +20,7 @@ import ( "fmt" "html/template" "io" + "strconv" "strings" "unicode/utf8" @@ -53,9 +54,8 @@ type pageContentReplacement struct { source pageparser.Item } -func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) { +func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) { var openSource hugio.OpenReadSeekCloser - var filename string if m.f != nil { meta := m.f.FileInfo().Meta() openSource = func() (hugio.ReadSeekCloser, error) { @@ -65,6 +65,44 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) { } return r, nil } + } + + if sourceKey == "" { + sourceKey = strconv.Itoa(int(pid)) + } + + pi := &contentParseInfo{ + h: h, + pid: pid, + sourceKey: sourceKey, + openSource: openSource, + } + + source, err := pi.contentSource(m) + if err != nil { + return nil, err + } + + items, err := pageparser.ParseBytes( + source, + pageparser.Config{}, + ) + if err != nil { + return nil, err + } + + pi.itemsStep1 = items + + if err := pi.mapFrontMatter(source); err != nil { + return nil, err + } + + return pi, nil +} + +func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) { + var filename string + if m.f != nil { filename = m.f.Filename() } @@ -72,15 +110,11 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) { pm: m.s.pageMap, StaleInfo: m, shortcodeState: newShortcodeHandler(filename, m.s), - parseInfo: &contentParseInfo{ - pid: pid, - }, - cacheBaseKey: m.pathInfo.PathNoLang(), - openSource: openSource, - enableEmoji: m.s.conf.EnableEmoji, + pi: pi, + enableEmoji: m.s.conf.EnableEmoji, } - source, err := c.contentSource() + source, err := c.pi.contentSource(m) if err != nil { return nil, err } @@ -95,23 +129,25 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) { type cachedContent struct { pm *pageMap - cacheBaseKey string - - // The source bytes. - openSource hugio.OpenReadSeekCloser - resource.StaleInfo shortcodeState *shortcodeHandler // Parsed content. - parseInfo *contentParseInfo + pi *contentParseInfo enableEmoji bool } type contentParseInfo struct { - pid uint64 + h *HugoSites + + pid uint64 + sourceKey string + + // The source bytes. + openSource hugio.OpenReadSeekCloser + frontMatter map[string]any // Whether the parsed content contains a summary separator. @@ -190,25 +226,15 @@ func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, } func (c *cachedContent) IsZero() bool { - return len(c.parseInfo.itemsStep2) == 0 + return len(c.pi.itemsStep2) == 0 } func (c *cachedContent) parseContentFile(source []byte) error { - if source == nil || c.openSource == nil { + if source == nil || c.pi.openSource == nil { return nil } - items, err := pageparser.ParseBytes( - source, - pageparser.Config{}, - ) - if err != nil { - return err - } - - c.parseInfo.itemsStep1 = items - - return c.parseInfo.mapItems(source, c.shortcodeState) + return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState) } func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error { @@ -242,7 +268,49 @@ func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser return nil } -func (rn *contentParseInfo) mapItems( +func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error { + if fe, ok := err.(herrors.FileError); ok { + return fe + } + + pos := posFromInput("", source, i.Pos()) + + return herrors.NewFileErrorFromPos(err, pos) +} + +func (rn *contentParseInfo) mapFrontMatter(source []byte) error { + if len(rn.itemsStep1) == 0 { + return nil + } + iter := pageparser.NewIterator(rn.itemsStep1) + +Loop: + for { + it := iter.Next() + switch { + case it.IsFrontMatter(): + if err := rn.parseFrontMatter(it, iter, source); err != nil { + return err + } + next := iter.Peek() + if !next.IsDone() { + rn.posMainContent = next.Pos() + } + // Done. + break Loop + case it.IsEOF(): + break Loop + case it.IsError(): + return rn.failMap(source, it.Err, it) + default: + + } + } + + return nil +} + +func (rn *contentParseInfo) mapItemsAfterFrontMatter( source []byte, s *shortcodeHandler, ) error { @@ -273,13 +341,7 @@ Loop: switch { case it.Type == pageparser.TypeIgnore: case it.IsFrontMatter(): - if err := rn.parseFrontMatter(it, iter, source); err != nil { - return err - } - next := iter.Peek() - if !next.IsDone() { - rn.posMainContent = next.Pos() - } + // Ignore. case it.Type == pageparser.TypeLeadSummaryDivider: posBody := -1 f := func(item pageparser.Item) bool { @@ -347,16 +409,16 @@ Loop: } func (c *cachedContent) mustSource() []byte { - source, err := c.contentSource() + source, err := c.pi.contentSource(c) if err != nil { panic(err) } return source } -func (c *cachedContent) contentSource() ([]byte, error) { - key := c.cacheBaseKey - v, err := c.pm.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) { +func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) { + key := c.sourceKey + v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) { b, err := c.readSourceAll() if err != nil { return nil, err @@ -365,7 +427,7 @@ func (c *cachedContent) contentSource() ([]byte, error) { return &resources.StaleValue[[]byte]{ Value: b, IsStaleFunc: func() bool { - return c.IsStale() + return s.IsStale() }, }, nil }) @@ -376,7 +438,7 @@ func (c *cachedContent) contentSource() ([]byte, error) { return v.Value, nil } -func (c *cachedContent) readSourceAll() ([]byte, error) { +func (c *contentParseInfo) readSourceAll() ([]byte, error) { if c.openSource == nil { return []byte{}, nil } @@ -424,7 +486,7 @@ type contentPlainPlainWords struct { func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) { ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal) - key := c.cacheBaseKey + "/" + cp.po.f.Name + key := c.pi.sourceKey + "/" + cp.po.f.Name versionv := cp.contentRenderedVersion v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { @@ -447,7 +509,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp }, } - if len(c.parseInfo.itemsStep2) == 0 { + if len(c.pi.itemsStep2) == 0 { // Nothing to do. return rs, nil } @@ -501,8 +563,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp var result contentSummary // hasVariants bool - if c.parseInfo.hasSummaryDivider { - isHTML := cp.po.p.m.markup == "html" + if c.pi.hasSummaryDivider { + isHTML := cp.po.p.m.pageConfig.Markup == "html" if isHTML { // Use the summary sections as provided by the user. i := bytes.Index(b, internalSummaryDividerPre) @@ -510,7 +572,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp b = b[i+len(internalSummaryDividerPre):] } else { - summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.markup, b) + summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b) if err != nil { cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err) } else { @@ -518,7 +580,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp result.summary = helpers.BytesToHTML(summary) } } - result.summaryTruncated = c.parseInfo.summaryTruncated + result.summaryTruncated = c.pi.summaryTruncated } result.content = helpers.BytesToHTML(b) rs.Value = result @@ -543,11 +605,11 @@ func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutpu var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback") func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) { - key := c.cacheBaseKey + "/" + cp.po.f.Name + key := c.pi.sourceKey + "/" + cp.po.f.Name versionv := cp.contentRenderedVersion v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { - source, err := c.contentSource() + source, err := c.pi.contentSource(c) if err != nil { return nil, err } @@ -572,7 +634,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( } if p.s.conf.Internal.Watch { - for _, s := range cp2.po.p.content.shortcodeState.shortcodes { + for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes { for _, templ := range s.templs { cp.trackDependency(templ.(identity.IdentityProvider)) } @@ -580,7 +642,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( } // Transfer shortcode names so HasShortcode works for shortcodes from included pages. - cp.po.p.content.shortcodeState.transferNames(cp2.po.p.content.shortcodeState) + cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState) if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 { cp.po.p.pageOutputTemplateVariationsState.Add(1) } @@ -589,7 +651,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) var hasVariants bool - ct.contentToRender, hasVariants, err = c.parseInfo.contentToRender(ctx, source, ct.contentPlaceholders) + ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders) if err != nil { return nil, err } @@ -598,7 +660,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( p.pageOutputTemplateVariationsState.Add(1) } - isHTML := cp.po.p.m.markup == "html" + isHTML := cp.po.p.m.pageConfig.Markup == "html" if !isHTML { createAndSetToC := func(tocProvider converter.TableOfContentsProvider) { @@ -661,7 +723,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( } func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) { - key := c.cacheBaseKey + "/" + cp.po.f.Name + key := c.pi.sourceKey + "/" + cp.po.f.Name versionv := cp.contentRenderedVersion @@ -681,7 +743,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) result.plain = tpl.StripHTML(string(rendered.content)) result.plainWords = strings.Fields(result.plain) - isCJKLanguage := cp.po.p.m.isCJKLanguage + isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage if isCJKLanguage { result.wordCount = 0 @@ -711,8 +773,8 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) if rendered.summary != "" { result.summary = rendered.summary result.summaryTruncated = rendered.summaryTruncated - } else if cp.po.p.m.summary != "" { - b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.summary), false) + } else if cp.po.p.m.pageConfig.Summary != "" { + b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false) if err != nil { return nil, err } diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index a549356fd93..179622d9b73 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -48,13 +48,11 @@ import ( var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) type pageMeta struct { - kind string // Page kind. term string // Set for kind == KindTerm. singular string // Set for kind == KindTerm and kind == KindTaxonomy. resource.Staler pageMetaParams - pageMetaFrontMatter // Set for standalone pages, e.g. robotsTXT. @@ -66,13 +64,15 @@ type pageMeta struct { pathInfo *paths.Path // Always set. This the canonical path to the Page. f *source.File + content *cachedContent // The source and the parsed page content. + s *Site // The site this page belongs to. } // Prepare for a rebuild of the data passed in from front matter. func (m *pageMeta) setMetaPostPrepareRebuild() { params := xmaps.Clone[map[string]any](m.paramsOriginal) - m.pageMetaParams.params = params + m.pageMetaParams.pageConfig.Params = params m.pageMetaFrontMatter = pageMetaFrontMatter{} } @@ -80,48 +80,28 @@ type pageMetaParams struct { setMetaPostCount int setMetaPostCascadeChanged bool - params map[string]any // Params contains configuration defined in the params section of page frontmatter. - cascade map[page.PageMatcher]maps.Params // cascade contains default configuration to be cascaded downwards. + pageConfig *pagemeta.PageConfig // These are only set in watch mode. - datesOriginal pageMetaDates + datesOriginal pagemeta.Dates paramsOriginal map[string]any // contains the original params as defined in the front matter. cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter. } // From page front matter. type pageMetaFrontMatter struct { - draft bool // Only published when running with -D flag - title string - linkTitle string - summary string - weight int - markup string - contentType string // type in front matter. - isCJKLanguage bool // whether the content is in a CJK language. - layout string - aliases []string - description string - keywords []string - translationKey string // maps to translation(s) of this page. - - buildConfig pagemeta.BuildConfig - configuredOutputFormats output.Formats // outputs defiend in front matter. - pageMetaDates // The 4 front matter dates that Hugo cares about. - resourcesMetadata []map[string]any // Raw front matter metadata that is going to be assigned to the page resources. - sitemap config.SitemapConfig // Sitemap overrides from front matter. - urlPaths pagemeta.URLPath + configuredOutputFormats output.Formats // outputs defiend in front matter. } func (m *pageMetaParams) init(preserveOringal bool) { if preserveOringal { - m.paramsOriginal = xmaps.Clone[maps.Params](m.params) - m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.cascade) + m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params) + m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade) } } func (p *pageMeta) Aliases() []string { - return p.aliases + return p.pageConfig.Aliases } func (p *pageMeta) Author() page.Author { @@ -150,8 +130,24 @@ func (p *pageMeta) BundleType() string { } } +func (p *pageMeta) Date() time.Time { + return p.pageConfig.Date +} + +func (p *pageMeta) PublishDate() time.Time { + return p.pageConfig.PublishDate +} + +func (p *pageMeta) Lastmod() time.Time { + return p.pageConfig.Lastmod +} + +func (p *pageMeta) ExpiryDate() time.Time { + return p.pageConfig.ExpiryDate +} + func (p *pageMeta) Description() string { - return p.description + return p.pageConfig.Description } func (p *pageMeta) Lang() string { @@ -159,7 +155,7 @@ func (p *pageMeta) Lang() string { } func (p *pageMeta) Draft() bool { - return p.draft + return p.pageConfig.Draft } func (p *pageMeta) File() *source.File { @@ -171,20 +167,20 @@ func (p *pageMeta) IsHome() bool { } func (p *pageMeta) Keywords() []string { - return p.keywords + return p.pageConfig.Keywords } func (p *pageMeta) Kind() string { - return p.kind + return p.pageConfig.Kind } func (p *pageMeta) Layout() string { - return p.layout + return p.pageConfig.Layout } func (p *pageMeta) LinkTitle() string { - if p.linkTitle != "" { - return p.linkTitle + if p.pageConfig.LinkTitle != "" { + return p.pageConfig.LinkTitle } return p.Title() @@ -194,7 +190,7 @@ func (p *pageMeta) Name() string { if p.resourcePath != "" { return p.resourcePath } - if p.kind == kinds.KindTerm { + if p.pageConfig.Kind == kinds.KindTerm { return p.pathInfo.Unmormalized().BaseNameNoIdentifier() } return p.Title() @@ -218,7 +214,7 @@ func (p *pageMeta) Param(key any) (any, error) { } func (p *pageMeta) Params() maps.Params { - return p.params + return p.pageConfig.Params } func (p *pageMeta) Path() string { @@ -248,18 +244,18 @@ func (p *pageMeta) Section() string { } func (p *pageMeta) Sitemap() config.SitemapConfig { - return p.sitemap + return p.pageConfig.Sitemap } func (p *pageMeta) Title() string { - return p.title + return p.pageConfig.Title } const defaultContentType = "page" func (p *pageMeta) Type() string { - if p.contentType != "" { - return p.contentType + if p.pageConfig.Type != "" { + return p.pageConfig.Type } if sect := p.Section(); sect != "" { @@ -270,36 +266,56 @@ func (p *pageMeta) Type() string { } func (p *pageMeta) Weight() int { - return p.weight + return p.pageConfig.Weight } -func (ps *pageState) setMetaPre() error { - pm := ps.m - p := ps - frontmatter := p.content.parseInfo.frontMatter - watching := p.s.watching() - +func (p *pageMeta) setMetaPre(pi *contentParseInfo, conf config.AllProvider) error { + frontmatter := pi.frontMatter if frontmatter != nil { + pcfg := p.pageConfig + if pcfg == nil { + panic("pageConfig not set") + } // Needed for case insensitive fetching of params values maps.PrepareParams(frontmatter) - pm.pageMetaParams.params = frontmatter - if p.IsNode() { - // Check for any cascade define on itself. - if cv, found := frontmatter["cascade"]; found { - var err error - cascade, err := page.DecodeCascade(cv) - if err != nil { - return err - } - pm.pageMetaParams.cascade = cascade + pcfg.Params = frontmatter + // Check for any cascade define on itself. + if cv, found := frontmatter["cascade"]; found { + var err error + cascade, err := page.DecodeCascade(cv) + if err != nil { + return err + } + pcfg.Cascade = cascade + } + // Look for path, lang and kind, all of which values we need early on. + if v, found := frontmatter["path"]; found { + pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v)) + pcfg.Params["path"] = pcfg.Path + } + if v, found := frontmatter["lang"]; found { + lang := strings.ToLower(cast.ToString(v)) + if _, ok := conf.PathParser().LanguageIndex[lang]; ok { + pcfg.Lang = lang + pcfg.Params["lang"] = pcfg.Lang } } - } else if pm.pageMetaParams.params == nil { - pm.pageMetaParams.params = make(maps.Params) + if v, found := frontmatter["kind"]; found { + s := cast.ToString(v) + if s != "" { + pcfg.Kind = kinds.GetKindMain(s) + if pcfg.Kind == "" { + return fmt.Errorf("unknown kind %q in front matter", s) + } + pcfg.Params["kind"] = pcfg.Kind + } + } + } else if p.pageMetaParams.pageConfig.Params == nil { + p.pageConfig.Params = make(maps.Params) } - pm.pageMetaParams.init(watching) + p.pageMetaParams.init(conf.Watching()) return nil } @@ -308,18 +324,18 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error ps.m.setMetaPostCount++ var cascadeHashPre uint64 if ps.m.setMetaPostCount > 1 { - cascadeHashPre = identity.HashUint64(ps.m.cascade) - ps.m.cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal) + cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade) + ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal) } // Apply cascades first so they can be overriden later. if cascade != nil { - if ps.m.cascade != nil { + if ps.m.pageConfig.Cascade != nil { for k, v := range cascade { - vv, found := ps.m.cascade[k] + vv, found := ps.m.pageConfig.Cascade[k] if !found { - ps.m.cascade[k] = v + ps.m.pageConfig.Cascade[k] = v } else { // Merge for ck, cv := range v { @@ -329,21 +345,21 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error } } } - cascade = ps.m.cascade + cascade = ps.m.pageConfig.Cascade } else { - ps.m.cascade = cascade + ps.m.pageConfig.Cascade = cascade } } if cascade == nil { - cascade = ps.m.cascade + cascade = ps.m.pageConfig.Cascade } if ps.m.setMetaPostCount > 1 { - ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.cascade) + ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade) if !ps.m.setMetaPostCascadeChanged { // No changes, restore any value that may be changed by aggregation. - ps.m.dates = ps.m.datesOriginal.dates + ps.m.pageConfig.Dates = ps.m.datesOriginal return nil } ps.m.setMetaPostPrepareRebuild() @@ -356,8 +372,8 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error continue } for kk, vv := range v { - if _, found := ps.m.params[kk]; !found { - ps.m.params[kk] = vv + if _, found := ps.m.pageConfig.Params[kk]; !found { + ps.m.pageConfig.Params[kk] = vv } } } @@ -371,7 +387,7 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error } // Store away any original values that may be changed from aggregation. - ps.m.datesOriginal = ps.m.pageMetaDates + ps.m.datesOriginal = ps.m.pageConfig.Dates return nil } @@ -392,13 +408,8 @@ func (p *pageState) setMetaPostParams() error { gitAuthorDate = p.gitInfo.AuthorDate } - pm.pageMetaDates = pageMetaDates{} - pm.urlPaths = pagemeta.URLPath{} - descriptor := &pagemeta.FrontMatterDescriptor{ - Params: pm.params, - Dates: &pm.pageMetaDates.dates, - PageURLs: &pm.urlPaths, + PageConfig: pm.pageConfig, BaseFilename: contentBaseName, ModTime: mtime, GitAuthorDate: gitAuthorDate, @@ -413,16 +424,27 @@ func (p *pageState) setMetaPostParams() error { p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) } - pm.buildConfig, err = pagemeta.DecodeBuildConfig(pm.params["_build"]) + var buildConfig any + if v, ok := pm.pageConfig.Params["_build"]; ok { + buildConfig = v + } else { + buildConfig = pm.pageConfig.Params["build"] + } + + pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig) if err != nil { return err } var sitemapSet bool + pcfg := pm.pageConfig + + params := pcfg.Params + var draft, published, isCJKLanguage *bool var userParams map[string]any - for k, v := range pm.params { + for k, v := range pcfg.Params { loki := strings.ToLower(k) if loki == "params" { @@ -431,7 +453,7 @@ func (p *pageState) setMetaPostParams() error { return err } userParams = vv - delete(pm.params, k) + delete(pcfg.Params, k) continue } @@ -450,43 +472,43 @@ func (p *pageState) setMetaPostParams() error { switch loki { case "title": - pm.title = cast.ToString(v) - pm.params[loki] = pm.title + pcfg.Title = cast.ToString(v) + params[loki] = pcfg.Title case "linktitle": - pm.linkTitle = cast.ToString(v) - pm.params[loki] = pm.linkTitle + pcfg.LinkTitle = cast.ToString(v) + params[loki] = pcfg.LinkTitle case "summary": - pm.summary = cast.ToString(v) - pm.params[loki] = pm.summary + pcfg.Summary = cast.ToString(v) + params[loki] = pcfg.Summary case "description": - pm.description = cast.ToString(v) - pm.params[loki] = pm.description + pcfg.Description = cast.ToString(v) + params[loki] = pcfg.Description case "slug": // Don't start or end with a - - pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-") - pm.params[loki] = pm.Slug() + pcfg.Slug = strings.Trim(cast.ToString(v), "-") + params[loki] = pm.Slug() case "url": url := cast.ToString(v) if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) } - pm.urlPaths.URL = url - pm.params[loki] = url + pcfg.URL = url + params[loki] = url case "type": - pm.contentType = cast.ToString(v) - pm.params[loki] = pm.contentType + pcfg.Type = cast.ToString(v) + params[loki] = pcfg.Type case "keywords": - pm.keywords = cast.ToStringSlice(v) - pm.params[loki] = pm.keywords + pcfg.Keywords = cast.ToStringSlice(v) + params[loki] = pcfg.Keywords case "headless": // Legacy setting for leaf bundles. // This is since Hugo 0.63 handled in a more general way for all // pages. isHeadless := cast.ToBool(v) - pm.params[loki] = isHeadless + params[loki] = isHeadless if p.File().TranslationBaseName() == "index" && isHeadless { - pm.buildConfig.List = pagemeta.Never - pm.buildConfig.Render = pagemeta.Never + pm.pageConfig.Build.List = pagemeta.Never + pm.pageConfig.Build.Render = pagemeta.Never } case "outputs": o := cast.ToStringSlice(v) @@ -501,43 +523,42 @@ func (p *pageState) setMetaPostParams() error { p.s.Log.Errorf("Failed to resolve output formats: %s", err) } else { pm.configuredOutputFormats = outFormats - pm.params[loki] = outFormats + params[loki] = outFormats } } case "draft": draft = new(bool) *draft = cast.ToBool(v) case "layout": - pm.layout = cast.ToString(v) - pm.params[loki] = pm.layout + pcfg.Layout = cast.ToString(v) + params[loki] = pcfg.Layout case "markup": - pm.markup = cast.ToString(v) - pm.params[loki] = pm.markup + pcfg.Markup = cast.ToString(v) + params[loki] = pcfg.Markup case "weight": - pm.weight = cast.ToInt(v) - pm.params[loki] = pm.weight + pcfg.Weight = cast.ToInt(v) + params[loki] = pcfg.Weight case "aliases": - pm.aliases = cast.ToStringSlice(v) - for i, alias := range pm.aliases { + pcfg.Aliases = cast.ToStringSlice(v) + for i, alias := range pcfg.Aliases { if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { return fmt.Errorf("http* aliases not supported: %q", alias) } - pm.aliases[i] = filepath.ToSlash(alias) + pcfg.Aliases[i] = filepath.ToSlash(alias) } - pm.params[loki] = pm.aliases + params[loki] = pcfg.Aliases case "sitemap": - p.m.sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v)) + pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v)) if err != nil { return fmt.Errorf("failed to decode sitemap config in front matter: %s", err) } - pm.params[loki] = p.m.sitemap sitemapSet = true case "iscjklanguage": isCJKLanguage = new(bool) *isCJKLanguage = cast.ToBool(v) case "translationkey": - pm.translationKey = cast.ToString(v) - pm.params[loki] = pm.translationKey + pcfg.TranslationKey = cast.ToString(v) + params[loki] = pcfg.TranslationKey case "resources": var resources []map[string]any handled := true @@ -563,8 +584,7 @@ func (p *pageState) setMetaPostParams() error { } if handled { - pm.params[loki] = resources - pm.resourcesMetadata = resources + pcfg.Resources = resources break } fallthrough @@ -586,51 +606,51 @@ func (p *pageState) setMetaPostParams() error { for i, u := range vv { a[i] = cast.ToString(u) } - pm.params[loki] = a + params[loki] = a } else { - pm.params[loki] = vv + params[loki] = vv } } else { - pm.params[loki] = []string{} + params[loki] = []string{} } default: - pm.params[loki] = vv + params[loki] = vv } } } for k, v := range userParams { - pm.params[strings.ToLower(k)] = v + params[strings.ToLower(k)] = v } if !sitemapSet { - pm.sitemap = p.s.conf.Sitemap + pcfg.Sitemap = p.s.conf.Sitemap } - pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup) + pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup) if draft != nil && published != nil { - pm.draft = *draft + pcfg.Draft = *draft p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) } else if draft != nil { - pm.draft = *draft + pcfg.Draft = *draft } else if published != nil { - pm.draft = !*published + pcfg.Draft = !*published } - pm.params["draft"] = pm.draft + params["draft"] = pcfg.Draft if isCJKLanguage != nil { - pm.isCJKLanguage = *isCJKLanguage - } else if p.s.conf.HasCJKLanguage && p.content.openSource != nil { - if cjkRe.Match(p.content.mustSource()) { - pm.isCJKLanguage = true + pcfg.IsCJKLanguage = *isCJKLanguage + } else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil { + if cjkRe.Match(p.m.content.mustSource()) { + pcfg.IsCJKLanguage = true } else { - pm.isCJKLanguage = false + pcfg.IsCJKLanguage = false } } - pm.params["iscjklanguage"] = p.m.isCJKLanguage + params["iscjklanguage"] = pcfg.IsCJKLanguage return nil } @@ -643,7 +663,7 @@ func (p *pageMeta) shouldList(global bool) bool { return false } - switch p.buildConfig.List { + switch p.pageConfig.Build.List { case pagemeta.Always: return true case pagemeta.Never: @@ -667,56 +687,56 @@ func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool { return false } - return p.kind == kinds.KindHome || p.kind == kinds.KindSection || p.kind == kinds.KindPage + return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage } func (p *pageMeta) noRender() bool { - return p.buildConfig.Render != pagemeta.Always + return p.pageConfig.Build.Render != pagemeta.Always } func (p *pageMeta) noLink() bool { - return p.buildConfig.Render == pagemeta.Never + return p.pageConfig.Build.Render == pagemeta.Never } func (p *pageMeta) applyDefaultValues() error { - if p.buildConfig.IsZero() { - p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil) + if p.pageConfig.Build.IsZero() { + p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil) } if !p.s.conf.IsKindEnabled(p.Kind()) { - (&p.buildConfig).Disable() + (&p.pageConfig.Build).Disable() } - if p.markup == "" { + if p.pageConfig.Markup == "" { if p.File() != nil { // Fall back to file extension - p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) + p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) } - if p.markup == "" { - p.markup = "markdown" + if p.pageConfig.Markup == "" { + p.pageConfig.Markup = "markdown" } } - if p.title == "" && p.f == nil { + if p.pageConfig.Title == "" && p.f == nil { switch p.Kind() { case kinds.KindHome: - p.title = p.s.Title() + p.pageConfig.Title = p.s.Title() case kinds.KindSection: sectionName := p.pathInfo.Unmormalized().BaseNameNoIdentifier() if p.s.conf.PluralizeListTitles { sectionName = flect.Pluralize(sectionName) } - p.title = p.s.conf.C.CreateTitle(sectionName) + p.pageConfig.Title = p.s.conf.C.CreateTitle(sectionName) case kinds.KindTerm: if p.term != "" { - p.title = p.s.conf.C.CreateTitle(p.term) + p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term) } else { panic("term not set") } case kinds.KindTaxonomy: - p.title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unmormalized().BaseNameNoIdentifier()), "-", " ", -1) + p.pageConfig.Title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unmormalized().BaseNameNoIdentifier()), "-", " ", -1) case kinds.KindStatus404: - p.title = "404 Page not found" + p.pageConfig.Title = "404 Page not found" } } @@ -767,7 +787,7 @@ func (m *pageMeta) outputFormats() output.Formats { } func (p *pageMeta) Slug() string { - return p.urlPaths.Slug + return p.pageConfig.Slug } func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { @@ -805,26 +825,6 @@ func getParamToLower(m resource.ResourceParamsProvider, key string) any { return getParam(m, key, true) } -type pageMetaDates struct { - dates resource.Dates -} - -func (d *pageMetaDates) Date() time.Time { - return d.dates.Date() -} - -func (d *pageMetaDates) Lastmod() time.Time { - return d.dates.Lastmod() -} - -func (d *pageMetaDates) PublishDate() time.Time { - return d.dates.PublishDate() -} - -func (d *pageMetaDates) ExpiryDate() time.Time { - return d.dates.ExpiryDate() -} - func (ps *pageState) initLazyProviders() error { ps.init.Add(func(ctx context.Context) (any, error) { pp, err := newPagePaths(ps) diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 89eeb2e0e37..3bef55f43fd 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -15,45 +15,106 @@ package hugolib import ( "fmt" + "path/filepath" "sync" "sync/atomic" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/page/pagemeta" ) var pageIDCounter atomic.Uint64 -func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { - if m.pathInfo == nil { +func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) { + m.Staler = &resources.AtomicStaler{} + if m.pageConfig == nil { + m.pageMetaParams = pageMetaParams{ + pageConfig: &pagemeta.PageConfig{ + Params: maps.Params{}, + }, + } + } + + var sourceKey string + if m.f != nil { + sourceKey = filepath.ToSlash(m.f.Filename()) + } + + pid := pageIDCounter.Add(1) + pi, err := m.parseFrontMatter(h, pid, sourceKey) + if err != nil { + return nil, nil, err + } + + if err := m.setMetaPre(pi, h.Conf); err != nil { + return nil, nil, m.wrapError(err, h.BaseFs.SourceFs) + } + pcfg := m.pageConfig + + if pcfg.Path != "" { + s := m.pageConfig.Path + if !paths.HasExt(s) { + var ( + isBranch bool + ext string = "md" + ) + if pcfg.Kind != "" { + isBranch = kinds.IsBranch(pcfg.Kind) + } else if m.pathInfo != nil { + isBranch = m.pathInfo.IsBranchBundle() + if m.pathInfo.Ext() != "" { + ext = m.pathInfo.Ext() + } + } else if m.f != nil { + pi := m.f.FileInfo().Meta().PathInfo + isBranch = pi.IsBranchBundle() + if pi.Ext() != "" { + ext = pi.Ext() + } + } + if isBranch { + s += "/_index." + ext + } else { + s += "/index." + ext + } + } + m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s) + } else if m.pathInfo == nil { if m.f != nil { m.pathInfo = m.f.FileInfo().Meta().PathInfo } + if m.pathInfo == nil { panic(fmt.Sprintf("missing pathInfo in %v", m)) } } - m.Staler = &resources.AtomicStaler{} - ps, err := func() (*pageState, error) { if m.s == nil { // Identify the Site/language to associate this Page with. var lang string - if m.f != nil { + if pcfg.Lang != "" { + lang = pcfg.Lang + } else if m.f != nil { meta := m.f.FileInfo().Meta() lang = meta.Lang m.s = h.Sites[meta.LangIndex] } else { lang = m.pathInfo.Lang() } + if lang == "" { + lang = h.Conf.DefaultContentLanguage() + } var found bool for _, ss := range h.Sites { if ss.Lang() == lang { @@ -62,51 +123,49 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { break } } + if !found { return nil, fmt.Errorf("no site found for language %q", lang) } - } // Identify Page Kind. - if m.kind == "" { - m.kind = kinds.KindSection + if m.pageConfig.Kind == "" { + m.pageConfig.Kind = kinds.KindSection if m.pathInfo.Base() == "/" { - m.kind = kinds.KindHome + m.pageConfig.Kind = kinds.KindHome } else if m.pathInfo.IsBranchBundle() { // A section, taxonomy or term. tc := m.s.pageMap.cfg.getTaxonomyConfig(m.Path()) if !tc.IsZero() { // Either a taxonomy or a term. if tc.pluralTreeKey == m.Path() { - m.kind = kinds.KindTaxonomy + m.pageConfig.Kind = kinds.KindTaxonomy } else { - m.kind = kinds.KindTerm + m.pageConfig.Kind = kinds.KindTerm } } } else if m.f != nil { - m.kind = kinds.KindPage + m.pageConfig.Kind = kinds.KindPage } } - if m.kind == kinds.KindPage && !m.s.conf.IsKindEnabled(m.kind) { + if m.pageConfig.Kind == kinds.KindPage && !m.s.conf.IsKindEnabled(m.pageConfig.Kind) { return nil, nil } - pid := pageIDCounter.Add(1) - - // Parse page content. - cachedContent, err := newCachedContent(m, pid) - if err != nil { - return nil, m.wrapError(err) - } - var dependencyManager identity.Manager = identity.NopManager if m.s.conf.Internal.Watch { dependencyManager = identity.NewManager(m.Path()) } + // Parse the rest of the page content. + m.content, err = m.newCachedContent(h, pi) + if err != nil { + return nil, m.wrapError(err, h.SourceFs) + } + ps := &pageState{ pid: pid, pageOutput: nopPageOutput, @@ -115,7 +174,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { Staler: m, dependencyManager: dependencyManager, pageCommon: &pageCommon{ - content: cachedContent, FileProvider: m, AuthorProvider: m, Scratcher: maps.NewScratcher(), @@ -168,10 +226,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { ps.ShortcodeInfoProvider = ps ps.AlternativeOutputFormatsProvider = ps - if err := ps.setMetaPre(); err != nil { - return nil, ps.wrapError(err) - } - if err := ps.initLazyProviders(); err != nil { return nil, ps.wrapError(err) } @@ -182,5 +236,9 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { m.MarkStale() } - return ps, err + if ps == nil { + return nil, nil, err + } + + return ps, ps.PathInfo(), err } diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go index 6e7980a6d29..b0d2009e432 100644 --- a/hugolib/page__paths.go +++ b/hugolib/page__paths.go @@ -127,7 +127,7 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error) Section: pageInfoCurrentSection, UglyURLs: s.h.Conf.IsUglyURLs(p.Section()), ForcePrefix: s.h.Conf.IsMultihost() || alwaysInSubDir, - URL: pm.urlPaths.URL, + URL: pm.pageConfig.URL, } if pm.Slug() != "" { diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 3d86cdece90..e0026ed1fbd 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -104,12 +104,12 @@ func (pco *pageContentOutput) Reset() { } func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { - return pco.po.p.content.mustContentToC(ctx, pco).tableOfContents + return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents } func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { - content := pco.po.p.content - source, err := content.contentSource() + content := pco.po.p.m.content + source, err := content.pi.contentSource(content) if err != nil { return "", err } @@ -125,7 +125,7 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT insertPlaceholders = true } c := make([]byte, 0, len(source)+(len(source)/10)) - for _, it := range content.parseInfo.itemsStep2 { + for _, it := range content.pi.itemsStep2 { switch v := it.(type) { case pageparser.Item: c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) @@ -169,12 +169,12 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT } func (pco *pageContentOutput) Content(ctx context.Context) (any, error) { - r, err := pco.po.p.content.contentRendered(ctx, pco) + r, err := pco.po.p.m.content.contentRendered(ctx, pco) return r.content, err } func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { - return pco.po.p.content.mustContentToC(ctx, pco).tableOfContentsHTML + return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML } func (p *pageContentOutput) Len(ctx context.Context) int { @@ -182,7 +182,7 @@ func (p *pageContentOutput) Len(ctx context.Context) int { } func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary { - r, err := pco.po.p.content.contentRendered(ctx, pco) + r, err := pco.po.p.m.content.contentRendered(ctx, pco) if err != nil { pco.fail(err) } @@ -190,7 +190,7 @@ func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSu } func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords { - r, err := pco.po.p.content.contentPlain(ctx, pco) + r, err := pco.po.p.m.content.contentPlain(ctx, pco) if err != nil { pco.fail(err) } @@ -270,7 +270,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te } conv := pco.po.p.getContentConverter() - if opts.Markup != "" && opts.Markup != pco.po.p.m.markup { + if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Markup { var err error conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) if err != nil { @@ -281,6 +281,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te var rendered []byte parseInfo := &contentParseInfo{ + h: pco.po.p.s.h, pid: pco.po.p.pid, } @@ -293,7 +294,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te } s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) - if err := parseInfo.mapItems(contentToRenderb, s); err != nil { + if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil { return "", err } @@ -320,7 +321,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te tokenHandler := func(ctx context.Context, token string) ([]byte, error) { if token == tocShortcodePlaceholder { - toc, err := pco.po.p.content.contentToC(ctx, pco) + toc, err := pco.po.p.m.content.contentToC(ctx, pco) if err != nil { return nil, err } @@ -350,7 +351,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te } // We need a consolidated view in $page.HasShortcode - pco.po.p.content.shortcodeState.transferNames(s) + pco.po.p.m.content.shortcodeState.transferNames(s) } else { c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) @@ -411,7 +412,7 @@ func (pco *pageContentOutput) initRenderHooks() error { var renderCacheMu sync.Mutex resolvePosition := func(ctx any) text.Position { - source := pco.po.p.content.mustSource() + source := pco.po.p.m.content.mustSource() var offset int switch v := ctx.(type) { diff --git a/hugolib/params_test.go b/hugolib/params_test.go index 32b4bd7c339..f80f14035fa 100644 --- a/hugolib/params_test.go +++ b/hugolib/params_test.go @@ -13,7 +13,11 @@ package hugolib -import "testing" +import ( + "testing" + + qt "github.com/frankban/quicktest" +) func TestFrontMatterParamsInItsOwnSection(t *testing.T) { t.Parallel() @@ -52,3 +56,106 @@ Summary: {{ .Summary }}| "Summary: frontmatter.summary|", ) } + +func TestFrontMatterParamsKindPath(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/" +disableKinds = ["taxonomy", "term"] + +-- content/p1.md -- +--- +title: "P1" +date: 2019-08-07 +path: "/a/b/c" +slug: "s1" +--- +-- content/mysection.md -- +--- +title: "My Section" +kind: "section" +date: 2022-08-07 +path: "/a/b" +--- +-- layouts/index.html -- +RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$ +Sections: {{ range site.Sections }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$ +{{ $ab := site.GetPage "a/b" }} +a/b pages: {{ range $ab.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ end }}$ +` + + b := Test(t, files) + + b.AssertFileContent("public/index.html", + "RegularPages: /a/b/c|/a/b/s1/|P1|2019-07-08| Slug: s1|$", + "Sections: /a|/a/|As", + "a/b pages: /a/b/c|/a/b/s1/|$", + ) +} + +func TestFrontMatterParamsLang(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/" +disableKinds = ["taxonomy", "term"] +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +[languages.nn] +weight = 2 +-- content/p1.md -- +--- +title: "P1 nn" +lang: "nn" +--- +-- content/p2.md -- +--- +title: "P2" +--- +-- layouts/index.html -- +RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ end }}$ + +` + + b := Test(t, files) + + b.AssertFileContent("public/en/index.html", + "RegularPages: /p2|/en/p2/|P2|$", + ) + b.AssertFileContent("public/nn/index.html", + "RegularPages: /p1|/nn/p1/|P1 nn|$", + ) +} + +func TestFrontMatterParamsLangNoCascade(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/" +disableKinds = ["taxonomy", "term"] +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +[languages.nn] +weight = 2 +-- content/_index.md -- ++++ +[[cascade]] +background = 'yosemite.jpg' +lang = 'nn' ++++ + +` + + b, err := TestE(t, files) + b.Assert(err, qt.IsNotNil) +} diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index c5125f717d0..0ec5d5ce462 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -315,7 +315,7 @@ func prepareShortcode( isRenderString bool, ) (shortcodeRenderer, error) { toParseErr := func(err error) error { - source := p.content.mustSource() + source := p.m.content.mustSource() return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos) } @@ -443,7 +443,7 @@ func doRenderShortcode( // unchanged. // 2 If inner does not have a newline, strip the wrapping

block and // the newline. - switch p.m.markup { + switch p.m.pageConfig.Markup { case "", "markdown": if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { cleaner, err := regexp.Compile(innerCleanupRegexp) diff --git a/hugolib/site_new.go b/hugolib/site_new.go index ddf45c28696..0cab713520b 100644 --- a/hugolib/site_new.go +++ b/hugolib/site_new.go @@ -40,6 +40,7 @@ import ( "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/publisher" + "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/page/siteidentities" @@ -281,6 +282,7 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites [] page.Pages](d.MemCache, "/pags/all", dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}, ), + cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](d.MemCache, "/cont/src", dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), translationKeyPages: maps.NewSliceCache[page.Page](), currentSite: sites[0], skipRebuildForFilenames: make(map[string]bool), diff --git a/hugolib/site_render.go b/hugolib/site_render.go index 379dd6e867b..138530680aa 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -129,7 +129,7 @@ func pageRenderer( continue } - if p.m.buildConfig.PublishResources { + if p.m.pageConfig.Build.PublishResources { if err := p.renderResources(); err != nil { s.SendError(p.errorf(err, "failed to render page resources")) continue diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go index 8529b0e2810..fbdb25d7247 100644 --- a/resources/page/page_matcher.go +++ b/resources/page/page_matcher.go @@ -82,6 +82,14 @@ func (m PageMatcher) Matches(p Page) bool { return true } +var disallowedCascadeKeys = map[string]bool{ + // These define the structure of the page tree and cannot + // currently be set in the cascade. + "kind": true, + "path": true, + "lang": true, +} + func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) { buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) { cascade := make(map[PageMatcher]maps.Params) @@ -101,6 +109,11 @@ func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsCon if err != nil { return nil, nil, err } + for k := range m { + if disallowedCascadeKeys[k] { + return nil, nil, fmt.Errorf("key %q not allowed in cascade config", k) + } + } cfgs = append(cfgs, c) } diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index d804f27a711..17859d846cd 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,15 +19,76 @@ import ( "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/config" "github.com/spf13/cast" ) +type Dates struct { + Date time.Time + Lastmod time.Time + PublishDate time.Time + ExpiryDate time.Time +} + +func (d Dates) IsDateOrLastModAfter(in Dates) bool { + return d.Date.After(in.Date) || d.Lastmod.After(in.Lastmod) +} + +func (d *Dates) UpdateDateAndLastmodIfAfter(in Dates) { + if in.Date.After(d.Date) { + d.Date = in.Date + } + if in.Lastmod.After(d.Lastmod) { + d.Lastmod = in.Lastmod + } +} + +func (d Dates) IsAllDatesZero() bool { + return d.Date.IsZero() && d.Lastmod.IsZero() && d.PublishDate.IsZero() && d.ExpiryDate.IsZero() +} + +// PageConfig configures a Page, typically from front matter. +// Note that all the top level fields are reserved Hugo keywords. +// Any custom configuration needs to be set in the Params map. +type PageConfig struct { + Dates // Dates holds the fource core dates for this page. + Title string // The title of the page. + LinkTitle string // The link title of the page. + Type string // The content type of the page. + Layout string // The layout to use for to render this page. + Markup string // The markup used in the content file. + Weight int // The weight of the page, used in sorting if set to a non-zero value. + Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path. + Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers. + Lang string // The language code for this page. This is usually derived from the module mount or filename. + Slug string // The slug for this page. + Description string // The description for this page. + Summary string // The summary for this page. + Draft bool // Whether or not the content is a draft. + Headless bool // Whether or not the page should be rendered. + IsCJKLanguage bool // Whether or not the content is in a CJK language. + TranslationKey string // The translation key for this page. + Keywords []string // The keywords for this page. + Aliases []string // The aliases for this page. + 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. + + // These build options are set in the front matter, + // but not passed on to .Params. + Resources []map[string]any + Cascade map[page.PageMatcher]maps.Params // Only relevant for branch nodes. + Sitemap config.SitemapConfig + Build BuildConfig + + // User defined params. + Params maps.Params +} + // FrontMatterHandler maps front matter into Page fields and .Params. // Note that we currently have only extracted the date logic. type FrontMatterHandler struct { @@ -47,9 +108,6 @@ type FrontMatterHandler struct { // FrontMatterDescriptor describes how to handle front matter for a given Page. // It has pointers to values in the receiving page which gets updated. type FrontMatterDescriptor struct { - // This is the Page's params. - Params map[string]any - // This is the Page's base filename (BaseFilename), e.g. page.md., or // if page is a leaf bundle, the bundle folder name (ContentBaseName). BaseFilename string @@ -60,13 +118,8 @@ type FrontMatterDescriptor struct { // May be set from the author date in Git. GitAuthorDate time.Time - // The below are pointers to values on Page and will be modified. - - // This is the Page's dates. - Dates *resource.Dates - - // This is the Page's Slug etc. - PageURLs *URLPath + // The below will be modified. + PageConfig *PageConfig // The Location to use to parse dates without time zone info. Location *time.Location @@ -83,8 +136,8 @@ var dateFieldAliases = map[string][]string{ // supplied front matter params. Note that this requires all lower-case keys // in the params map. func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error { - if d.Dates == nil { - panic("missing dates") + if d.PageConfig == nil { + panic("missing pageConfig") } if f.dateHandler == nil { @@ -297,7 +350,7 @@ func (f *FrontMatterHandler) createHandlers() error { if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date, func(d *FrontMatterDescriptor, t time.Time) { - d.Dates.FDate = t + d.PageConfig.Date = t setParamIfNotSet(fmDate, t, d) }); err != nil { return err @@ -306,7 +359,7 @@ func (f *FrontMatterHandler) createHandlers() error { if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmLastmod, t, d) - d.Dates.FLastmod = t + d.PageConfig.Lastmod = t }); err != nil { return err } @@ -314,7 +367,7 @@ func (f *FrontMatterHandler) createHandlers() error { if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmPubDate, t, d) - d.Dates.FPublishDate = t + d.PageConfig.PublishDate = t }); err != nil { return err } @@ -322,7 +375,7 @@ func (f *FrontMatterHandler) createHandlers() error { if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmExpiryDate, t, d) - d.Dates.FExpiryDate = t + d.PageConfig.ExpiryDate = t }); err != nil { return err } @@ -331,10 +384,10 @@ func (f *FrontMatterHandler) createHandlers() error { } func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) { - if _, found := d.Params[key]; found { + if _, found := d.PageConfig.Params[key]; found { return } - d.Params[key] = value + d.PageConfig.Params[key] = value } func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) { @@ -361,7 +414,7 @@ type frontmatterFieldHandlers int func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { return func(d *FrontMatterDescriptor) (bool, error) { - v, found := d.Params[key] + v, found := d.PageConfig.Params[key] if !found { return false, nil @@ -377,7 +430,7 @@ func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d setter(d, date) // This is the params key as set in front matter. - d.Params[key] = date + d.PageConfig.Params[key] = date return true, nil } @@ -392,9 +445,9 @@ func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMa setter(d, date) - if _, found := d.Params["slug"]; !found { + if _, found := d.PageConfig.Params["slug"]; !found { // Use slug from filename - d.PageURLs.Slug = slug + d.PageConfig.Slug = slug } return true, nil diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go index 1aff8b511a5..9e1151f22c2 100644 --- a/resources/page/pagemeta/page_frontmatter_test.go +++ b/resources/page/pagemeta/page_frontmatter_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,16 +22,15 @@ import ( "github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/resources/page/pagemeta" - "github.com/gohugoio/hugo/resources/resource" qt "github.com/frankban/quicktest" ) func newTestFd() *pagemeta.FrontMatterDescriptor { return &pagemeta.FrontMatterDescriptor{ - Params: make(map[string]any), - Dates: &resource.Dates{}, - PageURLs: &pagemeta.URLPath{}, + PageConfig: &pagemeta.PageConfig{ + Params: make(map[string]interface{}), + }, Location: time.UTC, } } @@ -105,16 +104,16 @@ func TestFrontMatterDatesHandlers(t *testing.T) { case ":git": d.GitAuthorDate = d1 } - d.Params["date"] = d2 + d.PageConfig.Params["date"] = d2 c.Assert(handler.HandleDates(d), qt.IsNil) - c.Assert(d.Dates.FDate, qt.Equals, d1) - c.Assert(d.Params["date"], qt.Equals, d2) + c.Assert(d.PageConfig.Dates.Date, qt.Equals, d1) + c.Assert(d.PageConfig.Params["date"], qt.Equals, d2) d = newTestFd() - d.Params["date"] = d2 + d.PageConfig.Params["date"] = d2 c.Assert(handler.HandleDates(d), qt.IsNil) - c.Assert(d.Dates.FDate, qt.Equals, d2) - c.Assert(d.Params["date"], qt.Equals, d2) + c.Assert(d.PageConfig.Dates.Date, qt.Equals, d2) + c.Assert(d.PageConfig.Params["date"], qt.Equals, d2) } } @@ -137,15 +136,15 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) { testDate, _ := time.Parse("2006-01-02", "2018-02-01") d := newTestFd() - d.Params["mydate"] = testDate - d.Params["date"] = testDate.Add(1 * 24 * time.Hour) - d.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour) - d.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour) + d.PageConfig.Params["mydate"] = testDate + d.PageConfig.Params["date"] = testDate.Add(1 * 24 * time.Hour) + d.PageConfig.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour) + d.PageConfig.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour) c.Assert(handler.HandleDates(d), qt.IsNil) - c.Assert(d.Dates.FDate.Day(), qt.Equals, 1) - c.Assert(d.Dates.FLastmod.Day(), qt.Equals, 2) - c.Assert(d.Dates.FPublishDate.Day(), qt.Equals, 4) - c.Assert(d.Dates.FExpiryDate.IsZero(), qt.Equals, true) + c.Assert(d.PageConfig.Dates.Date.Day(), qt.Equals, 1) + c.Assert(d.PageConfig.Dates.Lastmod.Day(), qt.Equals, 2) + c.Assert(d.PageConfig.Dates.PublishDate.Day(), qt.Equals, 4) + c.Assert(d.PageConfig.Dates.ExpiryDate.IsZero(), qt.Equals, true) } diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go index 94c6b00aaef..f5b6380bc20 100644 --- a/resources/page/pagemeta/pagemeta.go +++ b/resources/page/pagemeta/pagemeta.go @@ -17,13 +17,6 @@ import ( "github.com/mitchellh/mapstructure" ) -type URLPath struct { - URL string - Permalink string - Slug string - Section string -} - const ( Never = "never" Always = "always" diff --git a/resources/resource/dates.go b/resources/resource/dates.go index 88968750def..d84e26d57e4 100644 --- a/resources/resource/dates.go +++ b/resources/resource/dates.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import ( "github.com/gohugoio/hugo/common/htime" ) -var _ Dated = Dates{} - // Dated wraps a "dated resource". These are the 4 dates that makes // the date logic in Hugo. type Dated interface { @@ -37,27 +35,6 @@ type Dated interface { ExpiryDate() time.Time } -// Dates holds the 4 Hugo dates. -type Dates struct { - FDate time.Time - FLastmod time.Time - FPublishDate time.Time - FExpiryDate time.Time -} - -func (d *Dates) IsDateOrLastModAfter(in Dated) bool { - return d.Date().After(in.Date()) || d.Lastmod().After(in.Lastmod()) -} - -func (d *Dates) UpdateDateAndLastmodIfAfter(in Dated) { - if in.Date().After(d.Date()) { - d.FDate = in.Date() - } - if in.Lastmod().After(d.Lastmod()) { - d.FLastmod = in.Lastmod() - } -} - // IsFuture returns whether the argument represents the future. func IsFuture(d Dated) bool { if d.PublishDate().IsZero() { @@ -79,19 +56,3 @@ func IsExpired(d Dated) bool { func IsZeroDates(d Dated) bool { return d.Date().IsZero() && d.Lastmod().IsZero() && d.ExpiryDate().IsZero() && d.PublishDate().IsZero() } - -func (p Dates) Date() time.Time { - return p.FDate -} - -func (p Dates) Lastmod() time.Time { - return p.FLastmod -} - -func (p Dates) PublishDate() time.Time { - return p.FPublishDate -} - -func (p Dates) ExpiryDate() time.Time { - return p.FExpiryDate -} From 098a0ce0d3b22619603663184a7675e4a434d1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 30 Jan 2024 09:23:21 +0100 Subject: [PATCH 2/4] Add warnidf template function Also rename config `ignoreErrors` => `ignoreLogs` But the old still works. Closes #9189 --- common/loggers/logger.go | 15 +++++++++++++-- config/allconfig/allconfig.go | 16 ++++++++-------- config/allconfig/configlanguage.go | 4 ++-- config/allconfig/load.go | 1 + config/configProvider.go | 2 +- hugolib/integrationtest_builder.go | 20 ++++++++++++++++++-- hugolib/site_new.go | 2 +- tpl/data/data.go | 4 ++-- tpl/fmt/fmt.go | 12 +++++++++--- tpl/fmt/fmt_integration_test.go | 29 +++++++++++++++++++++-------- tpl/fmt/init.go | 7 +++++++ 11 files changed, 83 insertions(+), 29 deletions(-) diff --git a/common/loggers/logger.go b/common/loggers/logger.go index c4d81fb8305..f851513b3eb 100644 --- a/common/loggers/logger.go +++ b/common/loggers/logger.go @@ -179,9 +179,9 @@ type Logger interface { Debugln(v ...any) Error() logg.LevelLogger Errorf(format string, v ...any) + Erroridf(id, format string, v ...any) Errorln(v ...any) Errors() string - Errorsf(id, format string, v ...any) Info() logg.LevelLogger InfoCommand(command string) logg.LevelLogger Infof(format string, v ...any) @@ -197,6 +197,7 @@ type Logger interface { Warn() logg.LevelLogger WarnCommand(command string) logg.LevelLogger Warnf(format string, v ...any) + Warnidf(id, format string, v ...any) Warnln(v ...any) Deprecatef(fail bool, format string, v ...any) Trace(s logg.StringFunc) @@ -321,10 +322,20 @@ func (l *logAdapter) Errors() string { return l.errors.String() } -func (l *logAdapter) Errorsf(id, format string, v ...any) { +func (l *logAdapter) Erroridf(id, format string, v ...any) { + format += l.idfInfoStatement("error", id, format) l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...) } +func (l *logAdapter) Warnidf(id, format string, v ...any) { + format += l.idfInfoStatement("warning", id, format) + l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...) +} + +func (l *logAdapter) idfInfoStatement(what, id, format string) string { + return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id) +} + func (l *logAdapter) Trace(s logg.StringFunc) { l.tracel.Log(s) } diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 5788e792bdb..26e40230537 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -206,7 +206,7 @@ func (c Config) cloneForLang() *Config { x.DisableKinds = copyStringSlice(x.DisableKinds) x.DisableLanguages = copyStringSlice(x.DisableLanguages) x.MainSections = copyStringSlice(x.MainSections) - x.IgnoreErrors = copyStringSlice(x.IgnoreErrors) + x.IgnoreLogs = copyStringSlice(x.IgnoreLogs) x.IgnoreFiles = copyStringSlice(x.IgnoreFiles) x.Theme = copyStringSlice(x.Theme) @@ -299,9 +299,9 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { } } - ignoredErrors := make(map[string]bool) - for _, err := range c.IgnoreErrors { - ignoredErrors[strings.ToLower(err)] = true + ignoredLogIDs := make(map[string]bool) + for _, err := range c.IgnoreLogs { + ignoredLogIDs[strings.ToLower(err)] = true } baseURL, err := urls.NewBaseURLFromString(c.BaseURL) @@ -357,7 +357,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { BaseURLLiveReload: baseURL, DisabledKinds: disabledKinds, DisabledLanguages: disabledLangs, - IgnoredErrors: ignoredErrors, + IgnoredLogs: ignoredLogIDs, KindOutputFormats: kindOutputFormats, CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle), IsUglyURLSection: isUglyURL, @@ -394,7 +394,7 @@ type ConfigCompiled struct { KindOutputFormats map[string]output.Formats DisabledKinds map[string]bool DisabledLanguages map[string]bool - IgnoredErrors map[string]bool + IgnoredLogs map[string]bool CreateTitle func(s string) string IsUglyURLSection func(section string) bool IgnoreFile func(filename string) bool @@ -501,8 +501,8 @@ type RootConfig struct { // Enable to disable the build lock file. NoBuildLock bool - // A list of error IDs to ignore. - IgnoreErrors []string + // A list of log IDs to ignore. + IgnoreLogs []string // A list of regexps that match paths to ignore. // Deprecated: Use the settings on module imports. diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index 71bd232de17..0b4c7427832 100644 --- a/config/allconfig/configlanguage.go +++ b/config/allconfig/configlanguage.go @@ -89,8 +89,8 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool { return c.config.C.DisabledLanguages[lang] } -func (c ConfigLanguage) IgnoredErrors() map[string]bool { - return c.config.C.IgnoredErrors +func (c ConfigLanguage) IgnoredLogs() map[string]bool { + return c.config.C.IgnoredLogs } func (c ConfigLanguage) NoBuildLock() bool { diff --git a/config/allconfig/load.go b/config/allconfig/load.go index eceed31f4ba..cb267422f15 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -141,6 +141,7 @@ func (l configLoader) applyConfigAliases() error { {Key: "indexes", Value: "taxonomies"}, {Key: "logI18nWarnings", Value: "printI18nWarnings"}, {Key: "logPathWarnings", Value: "printPathWarnings"}, + {Key: "ignoreErrors", Value: "ignoreLogs"}, } for _, alias := range aliases { diff --git a/config/configProvider.go b/config/configProvider.go index 2536639ead9..21d832f17e0 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -67,7 +67,7 @@ type AllProvider interface { NewContentEditor() string Timeout() time.Duration StaticDirs() []string - IgnoredErrors() map[string]bool + IgnoredLogs() map[string]bool WorkingDir() string EnableEmoji() bool } diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index a46ae72751c..222c8ec2dd7 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -57,6 +57,13 @@ func TestOptDebug() TestOpt { } } +// TestOptWarn will enable warn logging in integration tests. +func TestOptWarn() TestOpt { + return func(c *IntegrationTestConfig) { + c.LogLevel = logg.LevelWarn + } +} + // TestOptWithNFDOnDarwin will normalize the Unicode filenames to NFD on Darwin. func TestOptWithNFDOnDarwin() TestOpt { return func(c *IntegrationTestConfig) { @@ -181,9 +188,18 @@ func (b *lockingBuffer) Write(p []byte) (n int, err error) { return } -func (s *IntegrationTestBuilder) AssertLogContains(text string) { +func (s *IntegrationTestBuilder) AssertLogContains(els ...string) { s.Helper() - s.Assert(s.logBuff.String(), qt.Contains, text) + for _, el := range els { + s.Assert(s.logBuff.String(), qt.Contains, el) + } +} + +func (s *IntegrationTestBuilder) AssertLogNotContains(els ...string) { + s.Helper() + for _, el := range els { + s.Assert(s.logBuff.String(), qt.Not(qt.Contains), el) + } } func (s *IntegrationTestBuilder) AssertLogMatches(expression string) { diff --git a/hugolib/site_new.go b/hugolib/site_new.go index 0cab713520b..debb81e77b1 100644 --- a/hugolib/site_new.go +++ b/hugolib/site_new.go @@ -124,7 +124,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { Stdout: cfg.LogOut, Stderr: cfg.LogOut, StoreErrors: conf.Running(), - SuppressStatements: conf.IgnoredErrors(), + SuppressStatements: conf.IgnoredLogs(), } logger = loggers.New(logOpts) } diff --git a/tpl/data/data.go b/tpl/data/data.go index 7eb730c4c06..78f1f3f48e2 100644 --- a/tpl/data/data.go +++ b/tpl/data/data.go @@ -94,7 +94,7 @@ func (ns *Namespace) GetCSV(sep string, args ...any) (d [][]string, err error) { if security.IsAccessDenied(err) { return nil, err } - ns.deps.Log.Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err) + ns.deps.Log.Erroridf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err) return nil, nil } @@ -132,7 +132,7 @@ func (ns *Namespace) GetJSON(args ...any) (any, error) { if security.IsAccessDenied(err) { return nil, err } - ns.deps.Log.Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err) + ns.deps.Log.Erroridf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err) return nil, nil } diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go index 4f18758e043..04dbd339c56 100644 --- a/tpl/fmt/fmt.go +++ b/tpl/fmt/fmt.go @@ -68,9 +68,7 @@ func (ns *Namespace) Errorf(format string, args ...any) string { // an information text that the error with the given id can be suppressed in config. // It returns an empty string. func (ns *Namespace) Erroridf(id, format string, args ...any) string { - format += "\nYou can suppress this error by adding the following to your site configuration:\nignoreErrors = ['%s']" - args = append(args, id) - ns.logger.Errorsf(id, format, args...) + ns.logger.Erroridf(id, format, args...) return "" } @@ -81,6 +79,14 @@ func (ns *Namespace) Warnf(format string, args ...any) string { return "" } +// Warnidf formats args according to a format specifier and logs an WARNING and +// an information text that the warning with the given id can be suppressed in config. +// It returns an empty string. +func (ns *Namespace) Warnidf(id, format string, args ...any) string { + ns.logger.Warnidf(id, format, args...) + return "" +} + // Warnmf is epxermimental and subject to change at any time. func (ns *Namespace) Warnmf(m any, format string, args ...any) string { return ns.logmf(ns.logger.Warn(), m, format, args...) diff --git a/tpl/fmt/fmt_integration_test.go b/tpl/fmt/fmt_integration_test.go index 40bfefcdc4e..74322770e08 100644 --- a/tpl/fmt/fmt_integration_test.go +++ b/tpl/fmt/fmt_integration_test.go @@ -16,6 +16,7 @@ package fmt_test import ( "testing" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugolib" ) @@ -32,13 +33,25 @@ ignoreErrors = ['error-b'] {{ erroridf "error-b" "%s" "b"}} ` - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ) + b, err := hugolib.TestE(t, files) - b.BuildE() - b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreErrors = \['error-a'\]\n$`) + b.Assert(err, qt.IsNotNil) + b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreLogs = \['error-a'\]\n$`) +} + +func TestWarnidf(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +ignoreLogs = ['warning-b'] +-- layouts/index.html -- +{{ warnidf "warning-a" "%s" "a"}} +{{ warnidf "warning-b" "%s" "b"}} + ` + + b := hugolib.Test(t, files, hugolib.TestOptWarn()) + b.AssertLogContains("WARN a", "You can suppress this warning", "ignoreLogs", "['warning-a']") + b.AssertLogNotContains("['warning-b']") } diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go index 8000627e211..701bd3b6a6a 100644 --- a/tpl/fmt/init.go +++ b/tpl/fmt/init.go @@ -66,6 +66,13 @@ func init() { }, ) + ns.AddMethodMapping(ctx.Warnidf, + []string{"warnidf"}, + [][2]string{ + {`{{ warnidf "my-warn-id" "%s." "warning" }}`, ``}, + }, + ) + ns.AddMethodMapping(ctx.Warnf, []string{"warnf"}, [][2]string{ From 686598faeeb79b1c71d09822f2483ff43f2709c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 30 Jan 2024 10:07:28 +0100 Subject: [PATCH 3/4] Emit a warning that can be turned off when overwriting built-in .Params values Fixes #11941 --- common/constants/constants.go | 4 +++- hugolib/page__meta.go | 4 ++++ hugolib/params_test.go | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/common/constants/constants.go b/common/constants/constants.go index e4f5a63a279..f8f057e053b 100644 --- a/common/constants/constants.go +++ b/common/constants/constants.go @@ -13,12 +13,14 @@ package constants -// Error IDs. +// Error/Warning IDs. // Do not change these values. const ( // IDs for remote errors in tpl/data. ErrRemoteGetJSON = "error-remote-getjson" ErrRemoteGetCSV = "error-remote-getcsv" + + WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides" ) // Field/method names with special meaning. diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 179622d9b73..e4cb2b83bc3 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -31,6 +31,7 @@ import ( "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/common/constants" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/paths" @@ -621,6 +622,9 @@ func (p *pageState) setMetaPostParams() error { } for k, v := range userParams { + if _, found := params[k]; found { + p.s.Log.Warnidf(constants.WarnFrontMatterParamsOverrides, "Hugo front matter key %q is overridden in params section.", k) + } params[strings.ToLower(k)] = v } diff --git a/hugolib/params_test.go b/hugolib/params_test.go index f80f14035fa..6f890b43b78 100644 --- a/hugolib/params_test.go +++ b/hugolib/params_test.go @@ -133,6 +133,28 @@ RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .T ) } +func TestFrontMatterTitleOverrideWarn(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/" +disableKinds = ["taxonomy", "term"] +-- content/p1.md -- +--- +title: "My title" +params: + title: "My title from params" +--- + + +` + + b := Test(t, files, TestOptWarn()) + + b.AssertLogContains("ARN Hugo front matter key \"title\" is overridden in params section", "You can suppress this warning") +} + func TestFrontMatterParamsLangNoCascade(t *testing.T) { t.Parallel() From 2cb6f234c753cd20f74568dc1238aa350908fcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 30 Jan 2024 10:42:24 +0100 Subject: [PATCH 4/4] Fix recent regression .Resources.Get for resources with spaces in filename Fixes #11944 --- hugolib/content_map_page.go | 2 +- hugolib/content_map_test.go | 19 +++++++++++++++++++ resources/page/pagemeta/page_frontmatter.go | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 9fee7400330..9accd190ec4 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -1548,7 +1548,7 @@ func (sa *sitePagesAssembler) assembleResources() error { return false, nil } - relPathOriginal := rs.path.PathRel(ps.m.pathInfo) + relPathOriginal := rs.path.Unmormalized().PathRel(ps.m.pathInfo.Unmormalized()) relPath := rs.path.BaseRel(ps.m.pathInfo) var targetBasePaths []string diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go index a41b2aae916..7843ad28589 100644 --- a/hugolib/content_map_test.go +++ b/hugolib/content_map_test.go @@ -280,3 +280,22 @@ P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}| filepath.FromSlash("P1: P1 md|md|/content/p1.md|"), ) } + +// Issue #11944 +func TestBundleResourcesGetWithSpacesInFilename(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term"] +-- content/bundle/index.md -- +-- content/bundle/data with Spaces.txt -- +Data. +-- layouts/index.html -- +{{ $bundle := site.GetPage "bundle" }} +{{ $r := $bundle.Resources.Get "data with Spaces.txt" }} +R: {{ with $r }}{{ .Content }}{{ end }}| +` + b := Test(t, files) + + b.AssertFileContent("public/index.html", "R: Data.") +} diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index 17859d846cd..8abd3807fe4 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -57,7 +57,7 @@ func (d Dates) IsAllDatesZero() bool { // Note that all the top level fields are reserved Hugo keywords. // Any custom configuration needs to be set in the Params map. type PageConfig struct { - Dates // Dates holds the fource core dates for this page. + Dates // Dates holds the four core dates for this page. Title string // The title of the page. LinkTitle string // The link title of the page. Type string // The content type of the page. @@ -66,6 +66,7 @@ type PageConfig struct { Weight int // The weight of the page, used in sorting if set to a non-zero value. Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path. Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers. + URL string // The URL to the rendered page, e.g. /sect/mypage.html. Lang string // The language code for this page. This is usually derived from the module mount or filename. Slug string // The slug for this page. Description string // The description for this page.