diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go index 951501406c6..d62b08e1e2b 100644 --- a/common/paths/pathparser.go +++ b/common/paths/pathparser.go @@ -108,7 +108,6 @@ func (pp *PathParser) parse(component, s string) (*Path, error) { var err error // Preserve the original case for titles etc. p.unnormalized, err = pp.doParse(component, s, pp.newPath(component)) - if err != nil { return nil, err } @@ -195,23 +194,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { } } - isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes - isContent := isContentComponent && files.IsContentExt(p.Ext()) - - if isContent { + if len(p.identifiers) > 0 { + isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes + isContent := isContentComponent && files.IsContentExt(p.Ext()) id := p.identifiers[len(p.identifiers)-1] b := p.s[p.posContainerHigh : id.Low-1] - switch b { - case "index": - p.bundleType = PathTypeLeaf - case "_index": - p.bundleType = PathTypeBranch - default: - p.bundleType = PathTypeContentSingle - } + if isContent { + switch b { + case "index": + p.bundleType = PathTypeLeaf + case "_index": + p.bundleType = PathTypeBranch + default: + p.bundleType = PathTypeContentSingle + } - if slashCount == 2 && p.IsLeafBundle() { - p.posSectionHigh = 0 + if slashCount == 2 && p.IsLeafBundle() { + p.posSectionHigh = 0 + } + } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) { + p.bundleType = PathTypeContentData } } @@ -246,6 +248,9 @@ const ( // Branch bundles, e.g. /blog/_index.md PathTypeBranch + + // Content data file, _content.gotmpl. + PathTypeContentData ) type Path struct { @@ -541,6 +546,10 @@ func (p *Path) IsLeafBundle() bool { return p.bundleType == PathTypeLeaf } +func (p *Path) IsContentData() bool { + return p.bundleType == PathTypeContentData +} + func (p Path) ForBundleType(t PathType) *Path { p.bundleType = t return &p diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go index 8c89ddd4109..11bfcca4f21 100644 --- a/common/paths/pathparser_test.go +++ b/common/paths/pathparser_test.go @@ -333,6 +333,22 @@ func TestParse(t *testing.T) { c.Assert(p.Path(), qt.Equals, "/a/b/c.txt") }, }, + { + "Content data file gotmpl", + "/a/b/_content.gotmpl", + func(c *qt.C, p *Path) { + c.Assert(p.Path(), qt.Equals, "/a/b/_content.gotmpl") + c.Assert(p.Ext(), qt.Equals, "gotmpl") + c.Assert(p.IsContentData(), qt.IsTrue) + }, + }, + { + "Content data file yaml", + "/a/b/_content.yaml", + func(c *qt.C, p *Path) { + c.Assert(p.IsContentData(), qt.IsFalse) + }, + }, } for _, test := range tests { c.Run(test.name, func(c *qt.C) { diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go index a8d231f7338..4012e6dadf9 100644 --- a/hugofs/files/classifier.go +++ b/hugofs/files/classifier.go @@ -82,6 +82,15 @@ func IsContentExt(ext string) bool { return contentFileExtensionsSet[ext] } +func IsGoTmplExt(ext string) bool { + return ext == "gotmpl" +} + +// Supported data file extensions for _content.* files. +func IsContentDataExt(ext string) bool { + return IsGoTmplExt(ext) +} + const ( ComponentFolderArchetypes = "archetypes" ComponentFolderStatic = "static" @@ -93,6 +102,8 @@ const ( FolderResources = "resources" FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc. + + NameContentData = "_content" ) var ( diff --git a/hugolib/content_map.go b/hugolib/content_map.go index 62cabec514c..953e803037c 100644 --- a/hugolib/content_map.go +++ b/hugolib/content_map.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "path" "path/filepath" @@ -23,10 +24,13 @@ import ( "github.com/bep/logg" "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/hugolib/pagesfromdata" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/hugofs" @@ -51,9 +55,11 @@ type contentMapConfig struct { var _ contentNodeI = (*resourceSource)(nil) type resourceSource struct { - path *paths.Path - opener hugio.OpenReadSeekCloser - fi hugofs.FileMetaInfo + langIndex int + path *paths.Path + opener hugio.OpenReadSeekCloser + fi hugofs.FileMetaInfo + rc *pagemeta.ResourceConfig r resource.Resource } @@ -64,11 +70,7 @@ func (r resourceSource) clone() *resourceSource { } func (r *resourceSource) LangIndex() int { - if r.r != nil && r.isPage() { - return r.r.(*pageState).s.languagei - } - - return r.fi.Meta().LangIndex + return r.langIndex } func (r *resourceSource) MarkStale() { @@ -162,7 +164,7 @@ func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) { return } -func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { +func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, whatChanged *whatChanged) error { if fi.IsDir() { return nil } @@ -199,9 +201,9 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { } key = pi.Base() - rs = &resourceSource{r: pageResource} + rs = &resourceSource{r: pageResource, langIndex: pageResource.s.languagei} } else { - rs = &resourceSource{path: pi, opener: r, fi: fim} + rs = &resourceSource{path: pi, opener: r, fi: fim, langIndex: fim.Meta().LangIndex} } tree.InsertIntoValuesDimension(key, rs) @@ -222,6 +224,132 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { if err := insertResource(fi); err != nil { return err } + case paths.PathTypeContentData: + m.s.Log.Trace(logg.StringFunc( + func() string { + return fmt.Sprintf("insert pages from data file: %q", fi.Meta().Filename) + }, + )) + + if !files.IsGoTmplExt(pi.Ext()) { + return fmt.Errorf("unsupported data file extension %q", pi.Ext()) + } + // TODO1 disabled languages. + + s := m.s.h.resolveSite(fi.Meta().Lang) + f := source.NewFileInfo(fi) + h := s.h + + // Make sure the layouts are initialized. + if _, err := h.init.layouts.Do(context.Background()); err != nil { + return err + } + if err := func() error { + contentAdapter := s.pageMap.treePagesFromTemplateOptions.Get(pi.Base()) + var rebuild bool + if contentAdapter != nil { + contentAdapter.Fi = fi + rebuild = true + } else { + contentAdapter = pagesfromdata.NewPagesFromTemplate( + pagesfromdata.PagesFromTemplateOptions{ + Fi: fi, + Site: s, // TODO1 wrapper without RegularPages etc. + DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps { + ss := s.(*Site) + return pagesfromdata.PagesFromTemplateDeps{ + TmplFinder: ss.TextTmpl(), + TmplExec: ss.Tmpl(), + } + }, + DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"), + Watching: s.Conf.Watching(), + HandlePage: func(ss page.Site, pc *pagemeta.PageConfig) error { + s := ss.(*Site) + pc.Path = path.Join(pi.Base(), pc.Path) + ps, pi, err := h.newPage( + &pageMeta{ + f: f, + s: s, + pageMetaParams: &pageMetaParams{ + pageConfig: pc, + }, + }, + ) + if err != nil { + return err + } + + if ps == nil { + // Disabled page. + return nil + } + + n, _, replaced := s.pageMap.treePages.InsertIntoValuesDimension(pi.Base(), ps) + + if h.isRebuild() && replaced { + whatChanged.Add(n.GetIdentity()) + } + + return nil + }, + HandleResource: func(ss page.Site, rc *pagemeta.ResourceConfig) error { + // TODO1 page resources? + s := ss.(*Site) + rc.Path = path.Join(pi.Base(), rc.Path) + rpi := s.Conf.PathParser().Parse(files.ComponentFolderContent, rc.Path) + rs := &resourceSource{path: rpi, rc: rc, opener: nil, fi: nil, langIndex: s.languagei} + n, _, replaced := s.pageMap.treeResources.InsertIntoValuesDimension(rc.Path, rs) + if h.isRebuild() && replaced { + whatChanged.Add(n.GetIdentity()) + } + return nil + }, + }, + ) + + s.pageMap.treePagesFromTemplateOptions.Insert(pi.Base(), contentAdapter) + + } + + if err := contentAdapter.Execute(context.Background()); err != nil { + return err + } + + if !rebuild && contentAdapter.BuildState.EnableAllLanguages { + // Clone and insert the adapter for the other sites. + for _, ss := range s.h.Sites { + if s == ss { + continue + } + + clone := contentAdapter.CloneForSite(ss) + + // Make sure it gets executed for the first time. + if err := clone.Execute(context.Background()); err != nil { + return err + } + + // Insert into the correct language tree so it get rebuilt on changes. + ss.pageMap.treePagesFromTemplateOptions.Insert(pi.Base(), clone) + + } + } + + if m.s.h.isRebuild() { + for _, p := range contentAdapter.BuildState.DeletedPaths { + // TODO1 language, resource etc. + pp := path.Join(pi.Base(), p) + if v, ok := m.treePages.Delete(pp); ok { + whatChanged.Add(v.GetIdentity()) + } + + } + } + return nil + }(); err != nil { + return err + } default: m.s.Log.Trace(logg.StringFunc( func() string { @@ -244,7 +372,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error { return nil } - m.treePages.InsertWithLock(pi.Base(), p) + m.treePages.InsertIntoValuesDimensionWithLock(pi.Base(), p) } return nil diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index a0bff747245..caf02b083dc 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -34,6 +34,7 @@ import ( "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugolib/doctree" + "github.com/gohugoio/hugo/hugolib/pagesfromdata" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources" @@ -100,6 +101,8 @@ type pageMap struct { cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]] contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]] + contentDataFileSeenItems *maps.Cache[string, map[uint64]bool] + cfg contentMapConfig } @@ -122,6 +125,10 @@ type pageTrees struct { // This tree contains all taxonomy entries, e.g "/tags/blue/page1" treeTaxonomyEntries *doctree.TreeShiftTree[*weightedContentNode] + // Stores the state for _content.gotmpl files. + // Mostly releveant for rebuilds. + treePagesFromTemplateOptions *doctree.TreeShiftTree[*pagesfromdata.PagesFromTemplate] + // A slice of the resource trees. resourceTrees doctree.MutableTrees } @@ -222,6 +229,7 @@ func (t pageTrees) Shape(d, v int) *pageTrees { t.treePages = t.treePages.Shape(d, v) t.treeResources = t.treeResources.Shape(d, v) t.treeTaxonomyEntries = t.treeTaxonomyEntries.Shape(d, v) + t.treePagesFromTemplateOptions = t.treePagesFromTemplateOptions.Shape(d, v) t.createMutableTrees() return &t @@ -587,9 +595,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources sort.SliceStable(res, lessFunc) - if len(ps.m.pageConfig.Resources) > 0 { + if len(ps.m.pageConfig.ResourcesMeta) > 0 { for i, r := range res { - res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.Resources, r) + res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.ResourcesMeta, r) } sort.SliceStable(res, lessFunc) } @@ -667,12 +675,13 @@ type contentNodeShifter struct { numLanguages int } -func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (bool, bool) { +func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (contentNodeI, bool, bool) { lidx := dimension[0] switch v := n.(type) { case contentNodeIs: - resource.MarkStale(v[lidx]) - wasDeleted := v[lidx] != nil + deleted := v[lidx] + resource.MarkStale(deleted) + wasDeleted := deleted != nil v[lidx] = nil isEmpty := true for _, vv := range v { @@ -681,10 +690,11 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) break } } - return wasDeleted, isEmpty + return deleted, wasDeleted, isEmpty case resourceSources: - resource.MarkStale(v[lidx]) - wasDeleted := v[lidx] != nil + deleted := v[lidx] + resource.MarkStale(deleted) + wasDeleted := deleted != nil v[lidx] = nil isEmpty := true for _, vv := range v { @@ -693,19 +703,19 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) break } } - return wasDeleted, isEmpty + return deleted, wasDeleted, isEmpty case *resourceSource: if lidx != v.LangIndex() { - return false, false + return nil, false, false } resource.MarkStale(v) - return true, true + return v, true, true case *pageState: if lidx != v.s.languagei { - return false, false + return nil, false, false } resource.MarkStale(v) - return true, true + return v, true, true default: panic(fmt.Sprintf("unknown type %T", n)) } @@ -778,7 +788,7 @@ func (s *contentNodeShifter) ForEeachInDimension(n contentNodeI, d int, f func(c } } -func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) contentNodeI { +func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) (contentNodeI, contentNodeI, bool) { langi := dimension[doctree.DimensionLanguage.Index()] switch vv := old.(type) { case *pageState: @@ -787,37 +797,39 @@ func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree panic(fmt.Sprintf("unknown type %T", new)) } if vv.s.languagei == newp.s.languagei && newp.s.languagei == langi { - return new + return new, vv, true } is := make(contentNodeIs, s.numLanguages) is[vv.s.languagei] = old is[langi] = new - return is + return is, old, false case contentNodeIs: + oldv := vv[langi] vv[langi] = new - return vv + return vv, oldv, oldv != nil case resourceSources: + oldv := vv[langi] vv[langi] = new.(*resourceSource) - return vv + return vv, oldv, oldv != nil case *resourceSource: newp, ok := new.(*resourceSource) if !ok { panic(fmt.Sprintf("unknown type %T", new)) } if vv.LangIndex() == newp.LangIndex() && newp.LangIndex() == langi { - return new + return new, vv, true } rs := make(resourceSources, s.numLanguages) rs[vv.LangIndex()] = vv rs[langi] = newp - return rs + return rs, vv, false default: panic(fmt.Sprintf("unknown type %T", old)) } } -func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { +func (s *contentNodeShifter) Insert(old, new contentNodeI) (contentNodeI, contentNodeI, bool) { switch vv := old.(type) { case *pageState: newp, ok := new.(*pageState) @@ -828,12 +840,12 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { if newp != old { resource.MarkStale(old) } - return new + return new, vv, true } is := make(contentNodeIs, s.numLanguages) is[newp.s.languagei] = new is[vv.s.languagei] = old - return is + return is, old, false case contentNodeIs: newp, ok := new.(*pageState) if !ok { @@ -844,7 +856,7 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { resource.MarkStale(oldp) } vv[newp.s.languagei] = new - return vv + return vv, oldp, oldp != nil case *resourceSource: newp, ok := new.(*resourceSource) if !ok { @@ -854,12 +866,12 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { if vv != newp { resource.MarkStale(vv) } - return new + return new, vv, true } rs := make(resourceSources, s.numLanguages) rs[newp.LangIndex()] = newp rs[vv.LangIndex()] = vv - return rs + return rs, vv, false case resourceSources: newp, ok := new.(*resourceSource) if !ok { @@ -870,7 +882,7 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { resource.MarkStale(oldp) } vv[newp.LangIndex()] = newp - return vv + return vv, oldp, oldp != nil default: panic(fmt.Sprintf("unknown type %T", old)) } @@ -890,6 +902,8 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) * 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}), + contentDataFileSeenItems: maps.NewCache[string, map[uint64]bool](), + cfg: contentMapConfig{ lang: s.Lang(), taxonomyConfig: taxonomiesConfig.Values(), @@ -960,8 +974,6 @@ type contentTreeReverseIndexMap struct { type sitePagesAssembler struct { *Site - watching bool - incomingChanges *whatChanged assembleChanges *whatChanged ctx context.Context } @@ -1032,6 +1044,7 @@ func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) { } } +// TODO1 do once? func (h *HugoSites) resolveAndClearStateForIdentities( ctx context.Context, l logg.LevelLogger, @@ -1080,6 +1093,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities( // 1. Handle the cache busters first, as those may produce identities for the page reset step. // 2. Then reset the page outputs, which may mark some resources as stale. // 3. Then GC the cache. + // TOOD1 if cachebuster != nil { if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { ll := l.WithField("substep", "gc dynacache cachebuster") @@ -1125,6 +1139,33 @@ func (h *HugoSites) resolveAndClearStateForIdentities( } changes = changes[:n] + if h.pageTrees.treePagesFromTemplateOptions.LenRaw() > 0 { + if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { + ll := l.WithField("substep", "resolve content adapter change set").WithField("changes", len(changes)) + checkedCount := 0 + matchCount := 0 + depsFinder := identity.NewFinder(identity.FinderConfig{}) + + h.pageTrees.treePagesFromTemplateOptions.WalkPrefixRaw(doctree.LockTypeRead, "", + func(s string, n *pagesfromdata.PagesFromTemplate) (bool, error) { + for _, id := range changes { + checkedCount++ + if r := depsFinder.Contains(id, n.DependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition { + n.BuildState.Rebuild = true + matchCount++ + break + } + } + return false, nil + }) + + ll = ll.WithField("checked", checkedCount).WithField("matches", matchCount) + return ll, nil + }); err != nil { + return err + } + } + if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { // changesLeft: The IDs that the pages is dependent on. // changesRight: The IDs that the pages depend on. @@ -1269,6 +1310,7 @@ func (sa *sitePagesAssembler) applyAggregates() error { rw := pw.Extend() rw.Tree = sa.pageMap.treeResources sa.lastmod = time.Time{} + rebuild := sa.s.h.isRebuild() pw.Handle = func(keyPage string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { pageBundle := n.(*pageState) @@ -1300,18 +1342,20 @@ func (sa *sitePagesAssembler) applyAggregates() error { } } - if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 { - oldDates := pageBundle.m.pageConfig.Dates + if rebuild { + if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 { + 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.pageConfig.Dates { - sa.assembleChanges.Add(pageBundle) - } - return nil - }, - ) + // 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.pageConfig.Dates { + sa.assembleChanges.Add(pageBundle) + } + return nil + }, + ) + } } // Combine the cascade map with front matter. @@ -1321,7 +1365,7 @@ func (sa *sitePagesAssembler) applyAggregates() error { // We receive cascade values from above. If this leads to a change compared // to the previous value, we need to mark the page and its dependencies as changed. - if pageBundle.m.setMetaPostCascadeChanged { + if rebuild && pageBundle.m.setMetaPostCascadeChanged { sa.assembleChanges.Add(pageBundle) } @@ -1553,7 +1597,7 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error { singular: viewName.singular, s: sa.Site, pathInfo: pi, - pageMetaParams: pageMetaParams{ + pageMetaParams: &pageMetaParams{ pageConfig: &pagemeta.PageConfig{ Kind: kinds.KindTerm, }, @@ -1647,6 +1691,10 @@ func (sa *sitePagesAssembler) assembleResources() error { } + if rs.rc != nil { + panic("implement me") + } + rd := resources.ResourceSourceDescriptor{ OpenReadSeekCloser: rs.opener, Path: rs.path, @@ -1775,7 +1823,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error { m := &pageMeta{ s: s, pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix), - pageMetaParams: pageMetaParams{ + pageMetaParams: &pageMetaParams{ pageConfig: &pagemeta.PageConfig{ Kind: kind, }, @@ -1893,7 +1941,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error { m := &pageMeta{ s: sa.Site, pathInfo: p, - pageMetaParams: pageMetaParams{ + pageMetaParams: &pageMetaParams{ pageConfig: &pagemeta.PageConfig{ Kind: kinds.KindHome, }, @@ -1903,7 +1951,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error { if err != nil { return err } - w.Tree.InsertWithLock(p.Base(), n) + w.Tree.InsertIntoValuesDimensionWithLock(p.Base(), n) sa.home = n } @@ -1926,7 +1974,7 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error { m := &pageMeta{ s: sa.Site, pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"), - pageMetaParams: pageMetaParams{ + pageMetaParams: &pageMetaParams{ pageConfig: &pagemeta.PageConfig{ Kind: kinds.KindTaxonomy, }, diff --git a/hugolib/doctree/nodeshiftree_test.go b/hugolib/doctree/nodeshiftree_test.go index 313be0bc4f7..13a84d5fa84 100644 --- a/hugolib/doctree/nodeshiftree_test.go +++ b/hugolib/doctree/nodeshiftree_test.go @@ -173,7 +173,7 @@ func TestTreeInsert(t *testing.T) { c.Assert(tree.Get("/notfound"), qt.IsNil) ab2 := &testValue{ID: "/a/b", Lang: 0} - v, ok := tree.InsertIntoValuesDimension("/a/b", ab2) + v, _, ok := tree.InsertIntoValuesDimension("/a/b", ab2) c.Assert(ok, qt.IsTrue) c.Assert(v, qt.DeepEquals, ab2) @@ -239,12 +239,12 @@ func (s *testShifter) ForEeachInDimension(n *testValue, d int, f func(n *testVal f(n) } -func (s *testShifter) Insert(old, new *testValue) *testValue { - return new +func (s *testShifter) Insert(old, new *testValue) (*testValue, *testValue, bool) { + return new, old, true } -func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) *testValue { - return new +func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) (*testValue, *testValue, bool) { + return new, old, true } func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (bool, bool) { diff --git a/hugolib/doctree/nodeshifttree.go b/hugolib/doctree/nodeshifttree.go index 1c11753055a..31830d24b96 100644 --- a/hugolib/doctree/nodeshifttree.go +++ b/hugolib/doctree/nodeshifttree.go @@ -38,16 +38,18 @@ type ( // Insert inserts new into the tree into the dimension it provides. // It may replace old. - // It returns a T (can be the same as old). - Insert(old, new T) T + // It returns the updated and existing T + // and a bool indicating if an existing record is updated. + Insert(old, new T) (T, T, bool) // Insert inserts new into the given dimension. // It may replace old. - // It returns a T (can be the same as old). - InsertInto(old, new T, dimension Dimension) T + // It returns the updated and existing T + // and a bool indicating if an existing record is updated. + InsertInto(old, new T, dimension Dimension) (T, T, bool) - // Delete deletes T from the given dimension and returns whether the dimension was deleted and if it's empty after the delete. - Delete(v T, dimension Dimension) (bool, bool) + // Delete deletes T from the given dimension and returns the deleted T and whether the dimension was deleted and if it's empty after the delete. + Delete(v T, dimension Dimension) (T, bool, bool) // Shift shifts T into the given dimension // and returns the shifted T and a bool indicating if the shift was successful and @@ -81,7 +83,11 @@ func New[T any](cfg Config[T]) *NodeShiftTree[T] { } } -func (r *NodeShiftTree[T]) Delete(key string) { +func (r *NodeShiftTree[T]) Delete(key string) (T, bool) { + return r.delete(key) +} + +func (r *NodeShiftTree[T]) DeleteRaw(key string) { r.delete(key) } @@ -103,23 +109,24 @@ func (r *NodeShiftTree[T]) DeletePrefix(prefix string) int { return false }) for _, key := range keys { - if ok := r.delete(key); ok { + if _, ok := r.delete(key); ok { count++ } } return count } -func (r *NodeShiftTree[T]) delete(key string) bool { +func (r *NodeShiftTree[T]) delete(key string) (T, bool) { var wasDeleted bool + var deleted T if v, ok := r.tree.Get(key); ok { var isEmpty bool - wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims) + deleted, wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims) if isEmpty { r.tree.Delete(key) } } - return wasDeleted + return deleted, wasDeleted } func (t *NodeShiftTree[T]) DeletePrefixAll(prefix string) int { @@ -141,22 +148,33 @@ func (t *NodeShiftTree[T]) Increment(d int) *NodeShiftTree[T] { return t.Shape(d, t.dims[d]+1) } -func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, bool) { +func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, T, bool) { s = mustValidateKey(cleanKey(s)) + var ( + updated bool + existing T + ) if vv, ok := r.tree.Get(s); ok { - v = r.shifter.InsertInto(vv.(T), v, r.dims) + v, existing, updated = r.shifter.InsertInto(vv.(T), v, r.dims) } r.tree.Insert(s, v) - return v, true + return v, existing, updated } -func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, bool) { +// InsertIntoValuesDimension inserts v into the tree at the given key and the +// dimension defined by the value. +// It returns the updated and existing T and a bool indicating if an existing record is updated. +func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, T, bool) { s = mustValidateKey(cleanKey(s)) + var ( + updated bool + existing T + ) if vv, ok := r.tree.Get(s); ok { - v = r.shifter.Insert(vv.(T), v) + v, existing, updated = r.shifter.Insert(vv.(T), v) } r.tree.Insert(s, v) - return v, true + return v, existing, updated } func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) { @@ -165,7 +183,7 @@ func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) { return r.tree.Insert(s, v) } -func (r *NodeShiftTree[T]) InsertWithLock(s string, v T) (T, bool) { +func (r *NodeShiftTree[T]) InsertIntoValuesDimensionWithLock(s string, v T) (T, T, bool) { r.mu.Lock() defer r.mu.Unlock() return r.InsertIntoValuesDimension(s, v) diff --git a/hugolib/doctree/support.go b/hugolib/doctree/support.go index 8083df127ac..adcc3b06cdb 100644 --- a/hugolib/doctree/support.go +++ b/hugolib/doctree/support.go @@ -113,7 +113,7 @@ type LockType int // MutableTree is a tree that can be modified. type MutableTree interface { - Delete(key string) + DeleteRaw(key string) DeleteAll(key string) DeletePrefix(prefix string) int DeletePrefixAll(prefix string) int @@ -140,9 +140,9 @@ var _ MutableTree = MutableTrees(nil) type MutableTrees []MutableTree -func (t MutableTrees) Delete(key string) { +func (t MutableTrees) DeleteRaw(key string) { for _, tree := range t { - tree.Delete(key) + tree.DeleteRaw(key) } } diff --git a/hugolib/doctree/treeshifttree.go b/hugolib/doctree/treeshifttree.go index f8a6d360bcb..79c529f954d 100644 --- a/hugolib/doctree/treeshifttree.go +++ b/hugolib/doctree/treeshifttree.go @@ -64,38 +64,33 @@ func (t *TreeShiftTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri return t.trees[t.v].WalkPrefix(lockType, s, f) } -func (t *TreeShiftTree[T]) Delete(key string) { +func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s string, v T) (bool, error)) error { for _, tt := range t.trees { - tt.tree.Delete(key) + if err := tt.WalkPrefix(lockType, s, f); err != nil { + return err + } } + return nil } -func (t *TreeShiftTree[T]) DeletePrefix(prefix string) int { +func (t *TreeShiftTree[T]) LenRaw() int { var count int for _, tt := range t.trees { - count += tt.tree.DeletePrefix(prefix) + count += tt.tree.Len() } return count } -func (t *TreeShiftTree[T]) Lock(writable bool) (commit func()) { - if writable { - for _, tt := range t.trees { - tt.mu.Lock() - } - return func() { - for _, tt := range t.trees { - tt.mu.Unlock() - } - } +func (t *TreeShiftTree[T]) Delete(key string) { + for _, tt := range t.trees { + tt.tree.Delete(key) } +} +func (t *TreeShiftTree[T]) DeletePrefix(prefix string) int { + var count int for _, tt := range t.trees { - tt.mu.RLock() - } - return func() { - for _, tt := range t.trees { - tt.mu.RUnlock() - } + count += tt.tree.DeletePrefix(prefix) } + return count } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index cf939ba9228..a2819e02c67 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -111,6 +111,24 @@ func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool { return h.skipRebuildForFilenames[ev.Name] } +func (h *HugoSites) isRebuild() bool { + return h.buildCounter.Load() > 0 +} + +func (h *HugoSites) resolveSite(lang string) *Site { + if lang == "" { + lang = h.Conf.DefaultContentLanguage() + } + + for _, s := range h.Sites { + if s.Lang() == lang { + return s + } + } + + return nil +} + // Only used in tests. type buildCounters struct { contentRenderCounter atomic.Uint64 diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 6a9afee9994..33421bb1c2a 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -30,6 +30,8 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/glob" + "github.com/gohugoio/hugo/hugolib/doctree" + "github.com/gohugoio/hugo/hugolib/pagesfromdata" "github.com/gohugoio/hugo/hugolib/segments" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/output" @@ -41,6 +43,7 @@ import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/para" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/rungroup" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page/siteidentities" @@ -248,15 +251,11 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil h.translationKeyPages.Reset() assemblers := make([]*sitePagesAssembler, len(h.Sites)) // Changes detected during assembly (e.g. aggregate date changes) - assembleChanges := &whatChanged{ - identitySet: make(map[identity.Identity]bool), - } + for i, s := range h.Sites { assemblers[i] = &sitePagesAssembler{ Site: s, - watching: s.watching(), - incomingChanges: bcfg.whatChanged, - assembleChanges: assembleChanges, + assembleChanges: bcfg.whatChanged, ctx: ctx, } } @@ -272,7 +271,7 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil return err } - changes := assembleChanges.Changes() + changes := bcfg.whatChanged.Changes() // Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation // of what needs to be re-built. @@ -696,8 +695,12 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf switch pathInfo.Component() { case files.ComponentFolderContent: logger.Println("Source changed", pathInfo.Path()) - if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 { - changes = append(changes, ids...) + isContentDataFile := pathInfo.IsContentData() + if !isContentDataFile { + if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 { + changes = append(changes, ids...) + } + } else if delete { } contentChanged = true @@ -912,6 +915,12 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf } } + if h.isRebuild() { + if err := h.processContentAdaptersOnRebuild(ctx, l, *config); err != nil { + return err + } + } + return nil } @@ -934,6 +943,24 @@ func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config return err } +func (h *HugoSites) processContentAdaptersOnRebuild(ctx context.Context, l logg.LevelLogger, buildConfig BuildCfg) error { + g := rungroup.Run[*pagesfromdata.PagesFromTemplate](ctx, rungroup.Config[*pagesfromdata.PagesFromTemplate]{ + NumWorkers: h.numWorkers, + Handle: func(ctx context.Context, p *pagesfromdata.PagesFromTemplate) error { + return p.Execute(ctx) + }, + }) + + h.pageTrees.treePagesFromTemplateOptions.WalkPrefixRaw(doctree.LockTypeRead, "", func(key string, p *pagesfromdata.PagesFromTemplate) (bool, error) { + if p.BuildState.Rebuild { + g.Enqueue(p) + } + return false, nil + }) + + return g.Wait() +} + func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig BuildCfg, filenames ...pathChange) error { if s.Deps == nil { panic("nil deps on site") @@ -944,7 +971,7 @@ func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildC // For inserts, we can pick an arbitrary pageMap. pageMap := s.Sites[0].pageMap - c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, filenames) + c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, buildConfig.whatChanged, filenames) if err := c.Collect(); err != nil { return err diff --git a/hugolib/page.go b/hugolib/page.go index 028d3fa959f..a1937485f90 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -581,7 +581,7 @@ func (p *pageState) getPageInfoForError() string { func (p *pageState) getContentConverter() converter.Converter { var err error p.contentConverterInit.Do(func() { - markup := p.m.pageConfig.Markup + markup := p.m.pageConfig.Content.Markup if markup == "html" { // Only used for shortcode inner content. markup = "markdown" diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 99ed824cd5d..b46da88818b 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -20,6 +20,7 @@ import ( "fmt" "html/template" "io" + "path/filepath" "strconv" "strings" "unicode/utf8" @@ -54,21 +55,31 @@ type pageContentReplacement struct { source pageparser.Item } -func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) { - var openSource hugio.OpenReadSeekCloser - if m.f != nil { - meta := m.f.FileInfo().Meta() - openSource = func() (hugio.ReadSeekCloser, error) { - r, err := meta.Open() - if err != nil { - return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err) +func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) { + var ( + sourceKey string + openSource hugio.OpenReadSeekCloser + hasContent = !m.pageConfig.Content.IsZero() + ) + + if m.f != nil && !hasContent { + sourceKey = filepath.ToSlash(m.f.Filename()) + if !hasContent { + meta := m.f.FileInfo().Meta() + openSource = func() (hugio.ReadSeekCloser, error) { + r, err := meta.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err) + } + return r, nil } - return r, nil } + } else if hasContent { + openSource = hugio.NewOpenReadSeekCloser(hugio.NewReadSeekerNoOpCloserFromString(m.pageConfig.Content.ValueString())) } if sourceKey == "" { - sourceKey = strconv.Itoa(int(pid)) + sourceKey = strconv.FormatUint(pid, 10) } pi := &contentParseInfo{ @@ -93,6 +104,11 @@ func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) pi.itemsStep1 = items + if hasContent { + // No front matter. + return pi, nil + } + if err := pi.mapFrontMatter(source); err != nil { return nil, err } @@ -567,7 +583,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp var result contentSummary // hasVariants bool if c.pi.hasSummaryDivider { - isHTML := cp.po.p.m.pageConfig.Markup == "html" + isHTML := cp.po.p.m.pageConfig.Content.Markup == "html" if isHTML { // Use the summary sections as provided by the user. i := bytes.Index(b, internalSummaryDividerPre) @@ -575,7 +591,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp b = b[i+len(internalSummaryDividerPre):] } else { - summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b) + summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Content.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 { @@ -665,7 +681,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( p.pageOutputTemplateVariationsState.Add(1) } - isHTML := cp.po.p.m.pageConfig.Markup == "html" + isHTML := cp.po.p.m.pageConfig.Content.Markup == "html" if !isHTML { createAndSetToC := func(tocProvider converter.TableOfContentsProvider) { @@ -788,7 +804,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) if err != nil { return nil, err } - html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Markup) + html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup) result.summary = helpers.BytesToHTML(html) } else { var summary string diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index a88fe528d83..027830ae96d 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -54,7 +54,7 @@ type pageMeta struct { singular string // Set for kind == KindTerm and kind == KindTaxonomy. resource.Staler - pageMetaParams + *pageMetaParams pageMetaFrontMatter // Set for standalone pages, e.g. robotsTXT. @@ -137,19 +137,19 @@ func (p *pageMeta) BundleType() string { } func (p *pageMeta) Date() time.Time { - return p.pageConfig.Date + return p.pageConfig.Dates.Date } func (p *pageMeta) PublishDate() time.Time { - return p.pageConfig.PublishDate + return p.pageConfig.Dates.PublishDate } func (p *pageMeta) Lastmod() time.Time { - return p.pageConfig.Lastmod + return p.pageConfig.Dates.Lastmod } func (p *pageMeta) ExpiryDate() time.Time { - return p.pageConfig.ExpiryDate + return p.pageConfig.Dates.ExpiryDate } func (p *pageMeta) Description() string { @@ -280,9 +280,6 @@ func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf if frontmatter != nil { pcfg := p.pageConfig - if pcfg == nil { - panic("pageConfig not set") - } // Needed for case insensitive fetching of params values maps.PrepareParams(frontmatter) pcfg.Params = frontmatter @@ -460,8 +457,10 @@ params: var sitemapSet bool pcfg := pm.pageConfig - params := pcfg.Params + if params == nil { + panic("params not set for " + p.Title()) + } var draft, published, isCJKLanguage *bool var userParams map[string]any @@ -554,8 +553,8 @@ params: pcfg.Layout = cast.ToString(v) params[loki] = pcfg.Layout case "markup": - pcfg.Markup = cast.ToString(v) - params[loki] = pcfg.Markup + pcfg.Content.Markup = cast.ToString(v) + params[loki] = pcfg.Content.Markup case "weight": pcfg.Weight = cast.ToInt(v) params[loki] = pcfg.Weight @@ -605,7 +604,7 @@ params: } if handled { - pcfg.Resources = resources + pcfg.ResourcesMeta = resources break } fallthrough @@ -652,7 +651,8 @@ params: pcfg.Sitemap = p.s.conf.Sitemap } - pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup) + // TODO1 + pcfg.Content.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Content.Markup) if draft != nil && published != nil { pcfg.Draft = *draft @@ -731,17 +731,17 @@ func (p *pageMeta) applyDefaultValues() error { (&p.pageConfig.Build).Disable() } - if p.pageConfig.Markup == "" { + if p.pageConfig.Content.Markup == "" { if p.File() != nil { // Fall back to file extension - p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) + p.pageConfig.Content.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) } - if p.pageConfig.Markup == "" { - p.pageConfig.Markup = "markdown" + if p.pageConfig.Content.Markup == "" { + p.pageConfig.Content.Markup = "markdown" } } - p.pageConfig.IsGoldmark = p.s.ContentSpec.Converters.IsGoldmark(p.pageConfig.Markup) + p.pageConfig.IsGoldmark = p.s.ContentSpec.Converters.IsGoldmark(p.pageConfig.Content.Markup) if p.pageConfig.Title == "" && p.f == nil { switch p.Kind() { diff --git a/hugolib/page__new.go b/hugolib/page__new.go index ac396288358..4f15a2aefe1 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -15,7 +15,6 @@ package hugolib import ( "fmt" - "path/filepath" "sync" "sync/atomic" @@ -36,21 +35,17 @@ var pageIDCounter atomic.Uint64 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{}, - }, + if m.pageMetaParams == nil { + m.pageMetaParams = &pageMetaParams{ + pageConfig: &pagemeta.PageConfig{}, } } - - var sourceKey string - if m.f != nil { - sourceKey = filepath.ToSlash(m.f.Filename()) + if m.pageConfig.Params == nil { + m.pageConfig.Params = maps.Params{} } pid := pageIDCounter.Add(1) - pi, err := m.parseFrontMatter(h, pid, sourceKey) + pi, err := m.parseFrontMatter(h, pid) if err != nil { return nil, nil, err } @@ -112,23 +107,13 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) { } 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 { - m.s = ss - found = true - break - } - } - if !found { + m.s = h.resolveSite(lang) + + if m.s == nil { return nil, fmt.Errorf("no site found for language %q", lang) } } diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 6b4b8f55ed9..909f908f274 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -283,7 +283,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.pageConfig.Markup { + if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Content.Markup { var err error conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) if err != nil { @@ -376,7 +376,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te } if opts.Display == "inline" { - markup := pco.po.p.m.pageConfig.Markup + markup := pco.po.p.m.pageConfig.Content.Markup if opts.Markup != "" { markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup) } diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index 1633feb3ed2..e5c1384890d 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -42,18 +42,20 @@ func newPagesCollector( logger loggers.Logger, infoLogger logg.LevelLogger, m *pageMap, + whatChanged *whatChanged, ids []pathChange, ) *pagesCollector { return &pagesCollector{ - ctx: ctx, - h: h, - fs: sp.BaseFs.Content.Fs, - m: m, - sp: sp, - logger: logger, - infoLogger: infoLogger, - ids: ids, - seenDirs: make(map[string]bool), + ctx: ctx, + h: h, + fs: sp.BaseFs.Content.Fs, + m: m, + sp: sp, + logger: logger, + infoLogger: infoLogger, + whatChanged: whatChanged, + ids: ids, + seenDirs: make(map[string]bool), } } @@ -68,6 +70,8 @@ type pagesCollector struct { fs afero.Fs + whatChanged *whatChanged + // List of paths that have changed. Used in partial builds. ids []pathChange seenDirs map[string]bool @@ -113,7 +117,7 @@ func (c *pagesCollector) Collect() (collectErr error) { c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{ NumWorkers: numWorkers, Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error { - if err := c.m.AddFi(fi); err != nil { + if err := c.m.AddFi(fi, c.whatChanged); err != nil { return hugofs.AddFileInfoToError(err, fi, c.fs) } numFilesProcessedTotal.Add(1) @@ -243,6 +247,20 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in return nil, nil } + n := 0 + for _, fi := range readdir { + if fi.Meta().PathInfo.IsContentData() { + // _content.json + // These are not part of any bundle, so just add them directly and remove them from the readdir slice. + if err := c.g.Enqueue(fi); err != nil { + return nil, err + } + } else { + n++ + } + } + readdir = readdir[:n] + // Pick the first regular file. var first hugofs.FileMetaInfo for _, fi := range readdir { @@ -260,6 +278,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in // Any bundle file will always be first. firstPi := first.Meta().PathInfo + if firstPi == nil { panic(fmt.Sprintf("collectDirDir: no path info for %q", first.Meta().Filename)) } diff --git a/hugolib/pagesfromdata/pagesfromgotmpl.go b/hugolib/pagesfromdata/pagesfromgotmpl.go new file mode 100644 index 00000000000..ee51c7960da --- /dev/null +++ b/hugolib/pagesfromdata/pagesfromgotmpl.go @@ -0,0 +1,292 @@ +// 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pagesfromdata + +import ( + "context" + "fmt" + "io" + "path/filepath" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/page/pagemeta" + "github.com/gohugoio/hugo/tpl" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cast" +) + +type PagesFromDataTemplateContext interface { + // AddPage adds a new page to the site. + // The first return value will always be an empty string. + AddPage(any) (string, error) + + // AddResource adds a new resource to the site. + // The first return value will always be an empty string. + AddResource(any) (string, error) + + // The site to which the pages will be added. + Site() page.Site + + // The same template may be executed multiple times for multiple languages. + // The Store can be used to store state between these invocations. + Store() *maps.Scratch + + // By default, the template will be executed for the language + // defined by the _content.gotmpl file (e.g. its mount definition). + // This method can be used to activate the template for all languages. + // The return value will always be an empty string. + EnableAllLanguages() string +} + +var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil) + +type pagesFromDataTemplateContext struct { + p *PagesFromTemplate +} + +func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) { + m, err := maps.ToStringMapE(v) + if err != nil { + return "", nil, err + } + pathv, ok := m["path"] + if !ok { + return "", nil, fmt.Errorf("path not set") + } + path, err := cast.ToStringE(pathv) + if err != nil || path == "" { + return "", nil, fmt.Errorf("invalid path %q", path) + } + return path, m, nil +} + +func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) { + path, m, err := p.toPathMap(v) + if err != nil { + return "", err + } + + if !p.p.BuildState.checkHasChangedAndSetSourceInfo(path, m) { + return "", nil + } + + var pd pagemeta.PageConfig + if err := mapstructure.WeakDecode(m, &pd); err != nil { + return "", err + } + + // TODO1 + pd.Build = pagemeta.DefaultBuildConfig + + if err := pd.Compile(true); err != nil { + return "", err + } + + return "", p.p.HandlePage(p.p.Site, &pd) +} + +func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) { + path, m, err := p.toPathMap(v) + if err != nil { + return "", err + } + + if !p.p.BuildState.checkHasChangedAndSetSourceInfo(path, m) { + return "", nil + } + + var rd pagemeta.ResourceConfig + if err := mapstructure.WeakDecode(m, &rd); err != nil { + return "", err + } + + if err := rd.Compile(); err != nil { + return "", err + } + + return "", p.p.HandleResource(p.p.Site, &rd) +} + +func (p *pagesFromDataTemplateContext) Site() page.Site { + return p.p.Site +} + +func (p *pagesFromDataTemplateContext) Store() *maps.Scratch { + return p.p.store +} + +func (p *pagesFromDataTemplateContext) EnableAllLanguages() string { + p.p.BuildState.EnableAllLanguages = true + return "" +} + +func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate { + opts.store = maps.NewScratch() + return &PagesFromTemplate{ + PagesFromTemplateOptions: opts, + PagesFromTemplateDeps: opts.DepsFromSite(opts.Site), + BuildState: &BuildState{ + sourceInfosCurrent: make(map[string]*sourceInfo), + }, + } +} + +type PagesFromTemplateOptions struct { + Fi hugofs.FileMetaInfo + + Site page.Site + DepsFromSite func(page.Site) PagesFromTemplateDeps + + store *maps.Scratch + + DependencyManager identity.Manager + + Watching bool // TODO1 use. + HandlePage func(s page.Site, p *pagemeta.PageConfig) error + HandleResource func(s page.Site, p *pagemeta.ResourceConfig) error +} + +type PagesFromTemplateDeps struct { + TmplFinder tpl.TemplateParseFinder + TmplExec tpl.TemplateExecutor +} + +type PagesFromTemplate struct { + PagesFromTemplateOptions + PagesFromTemplateDeps + BuildState *BuildState +} + +type BuildState struct { + Rebuild bool + + EnableAllLanguages bool + + // Paths deleted in the current build. + DeletedPaths []string + + sourceInfosCurrent map[string]*sourceInfo + sourceInfosPrevious map[string]*sourceInfo +} + +func (b *BuildState) printDebug() { + fmt.Println("Current:") + for k, v := range b.sourceInfosCurrent { + fmt.Printf("%s: %d\n", k, v.hash) + } + fmt.Println("Previous:") + for k, v := range b.sourceInfosPrevious { + fmt.Printf("%s: %d\n", k, v.hash) + } + + fmt.Println("Deleted:") + for _, v := range b.DeletedPaths { + fmt.Printf("%s\n", v) + } +} + +func (b *BuildState) hash(v any) uint64 { + return identity.HashUint64(v) +} + +func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool { + h := b.hash(v) + si, found := b.sourceInfosPrevious[changedPath] + if found { + b.sourceInfosCurrent[changedPath] = si + if si.hash == h { + return false + } + } else { + si = &sourceInfo{} + b.sourceInfosCurrent[changedPath] = si + } + si.hash = h + return true +} + +func (b *BuildState) resolveDeletedPaths() { + if b.sourceInfosPrevious == nil { + b.DeletedPaths = nil + return + } + var paths []string + for k := range b.sourceInfosPrevious { + if _, found := b.sourceInfosCurrent[k]; !found { + paths = append(paths, k) + } + } + b.DeletedPaths = paths +} + +func (b *BuildState) prepareNext() { + b.sourceInfosPrevious = b.sourceInfosCurrent + b.sourceInfosCurrent = make(map[string]*sourceInfo) +} + +type sourceInfo struct { + hash uint64 +} + +func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate { + // We deliberately make them share the same DepenencyManager and Store. + p.PagesFromTemplateOptions.Site = s + p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s) + p.BuildState = &BuildState{ + sourceInfosCurrent: make(map[string]*sourceInfo), + } + return &p +} + +func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager { + return p.DependencyManager +} + +func (p *PagesFromTemplate) Execute(ctx context.Context) error { + defer func() { + p.BuildState.prepareNext() + }() + + f, err := p.Fi.Meta().Open() + if err != nil { + return err + } + defer f.Close() + + tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.Fi.Meta().Filename), helpers.ReaderToString(f)) + if err != nil { + return err + } + + data := &pagesFromDataTemplateContext{ + p: p, + } + + ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p) + + if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil { + return err + } + + p.BuildState.resolveDeletedPaths() + // p.BuildState.printDebug() + + return nil +} + +////////////// diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go new file mode 100644 index 00000000000..f50124ea599 --- /dev/null +++ b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go @@ -0,0 +1,140 @@ +// 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pagesfromdata_test + +import ( + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +const filesPagesFromDataTempleBasic = ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term", "rss", "sitemap"] +baseURL = "https://example.com" +disableLiveReload = true +-- assets/mydata.yaml -- +p1: "p1" +-- layouts/partials/get-value.html -- +{{ $val := "p1" }} +{{ return $val }} +-- layouts/_default/single.html -- +Single: {{ .Title }}|{{ .Content }}|Params: {{ .Params.param1 }}| +Len Resources: {{ .Resources | len }} +-- layouts/_default/list.html -- +List: {{ .Title }}|{{ .Content }}| +RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Title }}:{{ .Path }}|{{ end }}$ +-- content/docs/_content.gotmpl -- +{{ $data := resources.Get "mydata.yaml" | transform.Unmarshal }} +{{ $pd := $data.p1 }} +{{ $pp := partial "get-value.html" }} +{{ $title := printf "%s:%s" $pd $pp }} +{{ $contentMarkdown := dict "type" "text" "value" "**Hello World**" "markup" "markdown" }} +{{ $contentHTML := dict "type" "text" "value" "Hello World!" "markup" "html" }} +{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title "content" $contentMarkdown "params" (dict "param1" "param1v" ) ) }} +{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2" "content" $contentHTML ) }} +{{ $resourceContent := dict "type" "resource" "value" $data }} +{{ $.AddResource (dict "path" "p1/data.yaml" "content" $resourceContent) }} + +` + +func TestPagesFromGoTmplBasic(t *testing.T) { + t.Parallel() + b := hugolib.Test(t, filesPagesFromDataTempleBasic) + b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1|", "Hello World", "Params: param1v|", "Len Resources: 1") + b.AssertFileContent("public/docs/p2/index.html", "Single: p2|", "Hello World!") +} + +func TestPagesFromGoTmplEditDataResource(t *testing.T) { + t.Parallel() + b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) + b.AssertRenderCountPage(4) + b.EditFileReplaceAll("assets/mydata.yaml", "p1: \"p1\"", "p1: \"p1edited\"").Build() + b.AssertFileContent("public/docs/p1/index.html", "Single: p1edited:p1|") + b.AssertRenderCountPage(1) +} + +func TestPagesFromGoTmplEditPartial(t *testing.T) { + t.Parallel() + b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) + b.EditFileReplaceAll("layouts/partials/get-value.html", "p1", "p1edited").Build() + b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1edited|") +} + +func TestPagesFromGoTmplRemovePage(t *testing.T) { + t.Parallel() + b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) + b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1|p2|$") + b.EditFileReplaceAll("content/docs/_content.gotmpl", `{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2" ) }}`, "").Build() + b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1|$") +} + +func TestPagesFromGoTmplMovePage(t *testing.T) { + t.Parallel() + b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) + b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2:/docs/p2|$") + b.EditFileReplaceAll("content/docs/_content.gotmpl", `"path" "p2"`, `"path" "p2moved"`).Build() + b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2:/docs/p2moved|$") +} + +func TestPagesFromGoTmplEnableAllLanguages(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +title = "Title" +[languages.fr] +title = "Titre" +weight = 2 +-- i18n/en.yaml -- +title: Title +-- i18n/fr.yaml -- +title: Titre +-- content/docs/_content.gotmpl -- +{{ .EnableAllLanguages }} +{{ $titleFromStore := .Store.Get "title" }} +{{ if not $titleFromStore }} + {{ $titleFromStore = "notfound"}} + {{ .Store.Set "title" site.Title }} +{{ end }} +{{ $title := printf "%s:%s:%s" site.Title (i18n "title") $titleFromStore }} +{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title ) }} +-- layouts/_default/single.html -- +Single: {{ .Title }}|{{ .Content }}| + +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title:Title:notfound||") + b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre:Titre:Title||") +} + +func TestPagesFromGoTmplRemoveGoTmpl(t *testing.T) { + t.Parallel() + t.Skip("TODO1") + b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) + b.RemoveFiles("content/docs/_content.gotmpl").Build() + b.AssertFileContent("public/index.html", "RegularPagesRecursive: |$") +} + +// TODO1 markup. +// TODO1 resource path must end with a file name. +// TODO1 prevent site.Home.RenderString +// TODO1 also make markdownify work without Home. diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_test.go new file mode 100644 index 00000000000..c60b56dbf00 --- /dev/null +++ b/hugolib/pagesfromdata/pagesfromgotmpl_test.go @@ -0,0 +1,32 @@ +// 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pagesfromdata + +import "testing" + +func BenchmarkHash(b *testing.B) { + m := map[string]any{ + "foo": "bar", + "bar": "foo", + "stringSlice": []any{"a", "b", "c"}, + "intSlice": []any{1, 2, 3}, + "largeText": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.", + } + + bs := BuildState{} + + for i := 0; i < b.N; i++ { + bs.hash(m) + } +} diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index af4454a89c2..3faf110c206 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -449,7 +449,7 @@ func doRenderShortcode( // unchanged. // 2 If inner does not have a newline, strip the wrapping

block and // the newline. - switch p.m.pageConfig.Markup { + switch p.m.pageConfig.Content.Markup { case "", "markdown": if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { cleaner, err := regexp.Compile(innerCleanupRegexp) diff --git a/hugolib/site.go b/hugolib/site.go index 2803878388d..c3af4bc068a 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -380,11 +380,21 @@ func (w *whatChanged) Add(ids ...identity.Identity) { w.mu.Lock() defer w.mu.Unlock() + if w.identitySet == nil { + w.identitySet = make(identity.Identities) + } + for _, id := range ids { w.identitySet[id] = true } } +func (w *whatChanged) Clear() { + w.mu.Lock() + defer w.mu.Unlock() + w.identitySet = identity.Identities{} +} + func (w *whatChanged) Changes() []identity.Identity { if w == nil || w.identitySet == nil { return nil diff --git a/hugolib/site_new.go b/hugolib/site_new.go index 496889295d2..e282fb22f78 100644 --- a/hugolib/site_new.go +++ b/hugolib/site_new.go @@ -32,6 +32,7 @@ import ( "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugolib/doctree" + "github.com/gohugoio/hugo/hugolib/pagesfromdata" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs/i18n" @@ -166,7 +167,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { treeResources: doctree.New( treeConfig, ), - treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)), + treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)), + treePagesFromTemplateOptions: doctree.NewTreeShiftTree[*pagesfromdata.PagesFromTemplate](doctree.DimensionLanguage.Index(), len(confm.Languages)), } pageTrees.createMutableTrees() diff --git a/parser/frontmatter.go b/parser/frontmatter.go index ced8b84fc47..18e55f9ad4f 100644 --- a/parser/frontmatter.go +++ b/parser/frontmatter.go @@ -104,7 +104,6 @@ func InterfaceToFrontMatter(in any, format metadecoders.Format, w io.Writer) err } err = InterfaceToConfig(in, format, w) - if err != nil { return err } diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index 123dd4b704d..f2f91c9f85e 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -14,9 +14,12 @@ package pagemeta import ( + "errors" + "fmt" "strings" "time" + "github.com/gohugoio/hugo/common/hstrings" "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" @@ -29,6 +32,13 @@ import ( "github.com/spf13/cast" ) +type DatesStrings struct { + Date string `json:"date"` + Lastmod string `json:"lastMod"` + PublishDate string `json:"publishDate"` + ExpiryDate string `json:"expiryDate"` +} + type Dates struct { Date time.Time Lastmod time.Time @@ -36,6 +46,8 @@ type Dates struct { ExpiryDate time.Time } +// date, err = htime.ToTimeInDefaultLocationE(v, d.Location) + func (d Dates) IsDateOrLastModAfter(in Dates) bool { return d.Date.After(in.Date) || d.Lastmod.After(in.Lastmod) } @@ -57,12 +69,12 @@ 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 four core dates for this page. + Dates Dates `json:"-"` // Dates holds the four core dates for this page. + DatesStrings 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. @@ -72,25 +84,91 @@ type PageConfig struct { 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. + Headless bool `json:"-"` // 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. + FrontMatterOnlyValues `json:"-"` + // 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 + // TODO1 + Cascade map[page.PageMatcher]maps.Params // Only relevant for branch nodes. + Sitemap config.SitemapConfig + Build BuildConfig // User defined params. Params maps.Params // Compiled values. IsGoldmark bool `json:"-"` + + Content Source +} + +// Compile validates and sets defaults etc. +func (p *PageConfig) Compile(pagesFromData bool) error { + if p.Path == "" { + return errors.New("path must be set") + } + if pagesFromData { + if strings.HasPrefix(p.Path, "/") { + return fmt.Errorf("path %q must not start with a /", p.Path) + } + if p.Content.Type == "" { + p.Content.Type = SourceTypeText + } + if p.Content.Type != SourceTypeText { + return errors.New("only text content is implemented in data files") + } + } + + return nil +} + +type ResourceConfig struct { + Path string + Name string + Content Source +} + +func (rc *ResourceConfig) Compile() error { + return nil +} + +type SourceType string + +const ( + SourceTypeText SourceType = "text" + SourceTypeURL SourceType = "url" +) + +type Source struct { + // Type may be either "text" or "url". + Type SourceType + // The markup used in Value. + Markup string + // The content. + Value any +} + +func (s Source) IsZero() bool { + return s.Type == "" +} + +func (s Source) ValueString() string { + ss, ok := hstrings.ToString(s.Value) + if !ok { + panic(errors.New("value is not a string")) + } + return ss +} + +type FrontMatterOnlyValues struct { + ResourcesMeta []map[string]any } // FrontMatterHandler maps front matter into Page fields and .Params. @@ -354,7 +432,7 @@ func (f *FrontMatterHandler) createHandlers() error { if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date, func(d *FrontMatterDescriptor, t time.Time) { - d.PageConfig.Date = t + d.PageConfig.Dates.Date = t setParamIfNotSet(fmDate, t, d) }); err != nil { return err @@ -363,7 +441,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.PageConfig.Lastmod = t + d.PageConfig.Dates.Lastmod = t }); err != nil { return err } @@ -371,7 +449,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.PageConfig.PublishDate = t + d.PageConfig.Dates.PublishDate = t }); err != nil { return err } @@ -379,7 +457,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.PageConfig.ExpiryDate = t + d.PageConfig.Dates.ExpiryDate = t }); err != nil { return err } diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go index f5b6380bc20..b6b9532310e 100644 --- a/resources/page/pagemeta/pagemeta.go +++ b/resources/page/pagemeta/pagemeta.go @@ -24,7 +24,7 @@ const ( Link = "link" ) -var defaultBuildConfig = BuildConfig{ +var DefaultBuildConfig = BuildConfig{ List: Always, Render: Always, PublishResources: true, @@ -69,7 +69,7 @@ func (b BuildConfig) IsZero() bool { } func DecodeBuildConfig(m any) (BuildConfig, error) { - b := defaultBuildConfig + b := DefaultBuildConfig if m == nil { return b, nil } diff --git a/source/fileInfo.go b/source/fileInfo.go index 44d08e62080..3263428cc28 100644 --- a/source/fileInfo.go +++ b/source/fileInfo.go @@ -37,6 +37,11 @@ type File struct { lazyInit sync.Once } +// TODO1 name. +func (fi *File) IsMultipart() bool { + return fi.fim.Meta().PathInfo.IsContentData() +} + // Filename returns a file's absolute path and filename on disk. func (fi *File) Filename() string { return fi.fim.Meta().Filename } diff --git a/tpl/template.go b/tpl/template.go index 5ef0eecb840..0ab1abf2f93 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -65,10 +65,14 @@ type TemplateHandlers struct { TxtTmpl TemplateParseFinder } +type TemplateExecutor interface { + ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error +} + // TemplateHandler finds and executes templates. type TemplateHandler interface { TemplateFinder - ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error + TemplateExecutor LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error) HasTemplate(name string) bool GetIdentity(name string) (identity.Identity, bool)