summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2024-03-17 11:12:33 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2024-05-14 13:12:08 +0200
commite2d66e3218e180bbfca06ca3a29ce01957c513e9 (patch)
treeed29bb99cf16b75b6334e2fc618d31e80203e5d5
parent55dea41c1ab703f13b841389c6888815a033cf86 (diff)
Create pages from _content.gotmpl
Closes #12427 Closes #12485 Closes #6310 Closes #5074
-rw-r--r--commands/hugobuilder.go2
-rw-r--r--commands/server.go8
-rw-r--r--common/maps/cache.go14
-rw-r--r--common/paths/pathparser.go51
-rw-r--r--common/paths/pathparser_test.go19
-rw-r--r--config/allconfig/allconfig.go4
-rw-r--r--config/allconfig/allconfig_integration_test.go18
-rw-r--r--config/allconfig/configlanguage.go4
-rw-r--r--config/configProvider.go10
-rw-r--r--create/content.go4
-rw-r--r--helpers/content.go23
-rw-r--r--helpers/content_test.go2
-rw-r--r--helpers/general_test.go6
-rw-r--r--hugofs/files/classifier.go56
-rw-r--r--hugofs/files/classifier_test.go11
-rw-r--r--hugofs/walk.go10
-rw-r--r--hugolib/config_test.go2
-rw-r--r--hugolib/content_map.go197
-rw-r--r--hugolib/content_map_page.go184
-rw-r--r--hugolib/doctree/nodeshiftree_test.go14
-rw-r--r--hugolib/doctree/nodeshifttree.go55
-rw-r--r--hugolib/doctree/simpletree.go17
-rw-r--r--hugolib/doctree/support.go6
-rw-r--r--hugolib/doctree/treeshifttree.go57
-rw-r--r--hugolib/hugo_sites.go19
-rw-r--r--hugolib/hugo_sites_build.go138
-rw-r--r--hugolib/hugo_smoke_test.go2
-rw-r--r--hugolib/page.go14
-rw-r--r--hugolib/page__content.go45
-rw-r--r--hugolib/page__meta.go76
-rw-r--r--hugolib/page__new.go69
-rw-r--r--hugolib/page__per_output.go12
-rw-r--r--hugolib/pages_capture.go49
-rw-r--r--hugolib/pagesfromdata/pagesfromgotmpl.go331
-rw-r--r--hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go479
-rw-r--r--hugolib/pagesfromdata/pagesfromgotmpl_test.go32
-rw-r--r--hugolib/shortcode.go4
-rw-r--r--hugolib/site.go29
-rw-r--r--hugolib/site_new.go24
-rw-r--r--hugolib/site_sections.go2
-rw-r--r--langs/i18n/translationProvider.go1
-rw-r--r--markup/markup.go29
-rw-r--r--media/builtin.go31
-rw-r--r--media/config.go122
-rw-r--r--media/config_test.go19
-rw-r--r--media/mediaType.go55
-rw-r--r--media/mediaType_test.go12
-rw-r--r--parser/frontmatter.go1
-rw-r--r--resources/page/page_nop.go2
-rw-r--r--resources/page/pagemeta/page_frontmatter.go321
-rw-r--r--resources/page/pagemeta/page_frontmatter_test.go31
-rw-r--r--resources/page/pagemeta/pagemeta.go4
-rw-r--r--resources/postpub/fields_test.go2
-rw-r--r--resources/resource.go16
-rw-r--r--resources/resource/resourcetypes.go12
-rw-r--r--resources/resource_metadata.go29
-rw-r--r--resources/resource_spec.go16
-rw-r--r--resources/transform.go8
-rw-r--r--source/fileInfo.go9
-rw-r--r--tpl/template.go6
60 files changed, 2389 insertions, 436 deletions
diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go
index 657048d48..32b7e1de8 100644
--- a/commands/hugobuilder.go
+++ b/commands/hugobuilder.go
@@ -854,7 +854,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
h.BaseFs.SourceFilesystems,
dynamicEvents)
- onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
+ onePageName := pickOneWriteOrCreatePath(h.Conf.ContentTypes(), partitionedEvents.ContentEvents)
c.printChangeDetected("")
c.changeDetector.PrepareNew()
diff --git a/commands/server.go b/commands/server.go
index ccd2bde7d..5832c83d8 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -46,12 +46,12 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
+
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
@@ -1188,16 +1188,16 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs
return
}
-func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
+func pickOneWriteOrCreatePath(contentTypes config.ContentTypesProvider, events []fsnotify.Event) string {
name := ""
for _, ev := range events {
if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create {
- if files.IsIndexContentFile(ev.Name) {
+ if contentTypes.IsIndexContentFile(ev.Name) {
return ev.Name
}
- if files.IsContentFile(ev.Name) {
+ if contentTypes.IsContentFile(ev.Name) {
name = ev.Name
}
diff --git a/common/maps/cache.go b/common/maps/cache.go
index 7e23a2662..3723d318e 100644
--- a/common/maps/cache.go
+++ b/common/maps/cache.go
@@ -27,7 +27,12 @@ func NewCache[K comparable, T any]() *Cache[K, T] {
}
// Delete deletes the given key from the cache.
+// If c is nil, this method is a no-op.
func (c *Cache[K, T]) Get(key K) (T, bool) {
+ if c == nil {
+ var zero T
+ return zero, false
+ }
c.RLock()
v, found := c.m[key]
c.RUnlock()
@@ -60,6 +65,15 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
+// ForEeach calls the given function for each key/value pair in the cache.
+func (c *Cache[K, T]) ForEeach(f func(K, T)) {
+ c.RLock()
+ defer c.RUnlock()
+ for k, v := range c.m {
+ f(k, v)
+ }
+}
+
// SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct {
m map[string][]T
diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go
index 951501406..5fa798fb0 100644
--- a/common/paths/pathparser.go
+++ b/common/paths/pathparser.go
@@ -25,8 +25,6 @@ import (
"github.com/gohugoio/hugo/identity"
)
-var defaultPathParser PathParser
-
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
@@ -34,11 +32,9 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
-}
-// Parse parses component c with path s into Path using the default path parser.
-func Parse(c, s string) *Path {
- return defaultPathParser.Parse(c, s)
+ // Reports whether the given ext is a content file.
+ IsContentExt func(string) bool
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -108,7 +104,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 +190,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 && pp.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 +244,9 @@ const (
// Branch bundles, e.g. /blog/_index.md
PathTypeBranch
+
+ // Content data file, _content.gotmpl.
+ PathTypeContentData
)
type Path struct {
@@ -521,10 +522,6 @@ func (p *Path) Identifiers() []string {
return ids
}
-func (p *Path) IsHTML() bool {
- return files.IsHTML(p.Ext())
-}
-
func (p *Path) BundleType() PathType {
return p.bundleType
}
@@ -541,6 +538,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 8c89ddd41..e8fee96e1 100644
--- a/common/paths/pathparser_test.go
+++ b/common/paths/pathparser_test.go
@@ -27,6 +27,9 @@ var testParser = &PathParser{
"no": 0,
"en": 1,
},
+ IsContentExt: func(ext string) bool {
+ return ext == "md"
+ },
}
func TestParse(t *testing.T) {
@@ -333,6 +336,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/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index d5d3dc4e7..76153f5c0 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -367,6 +367,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
DisabledLanguages: disabledLangs,
IgnoredLogs: ignoredLogIDs,
KindOutputFormats: kindOutputFormats,
+ ContentTypes: media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
@@ -402,6 +403,7 @@ type ConfigCompiled struct {
BaseURLLiveReload urls.BaseURL
ServerInterface string
KindOutputFormats map[string]output.Formats
+ ContentTypes media.ContentTypes
DisabledKinds map[string]bool
DisabledLanguages map[string]bool
IgnoredLogs map[string]bool
@@ -759,7 +761,7 @@ func (c *Configs) Init() error {
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
- c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled}
+ c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go
index 4f2f1a06e..af4655fe8 100644
--- a/config/allconfig/allconfig_integration_test.go
+++ b/config/allconfig/allconfig_integration_test.go
@@ -84,3 +84,21 @@ logPathWarnings = true
b.Assert(conf.PrintI18nWarnings, qt.Equals, true)
b.Assert(conf.PrintPathWarnings, qt.Equals, true)
}
+
+func TestRedefineContentTypes(t *testing.T) {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+[mediaTypes]
+[mediaTypes."text/html"]
+suffixes = ["html", "xhtml"]
+`
+
+ b := hugolib.Test(t, files)
+
+ conf := b.H.Configs.Base
+ contentTypes := conf.C.ContentTypes
+
+ b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
+ b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
+}
diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go
index c7f1c276a..a215fb5e4 100644
--- a/config/allconfig/configlanguage.go
+++ b/config/allconfig/configlanguage.go
@@ -144,6 +144,10 @@ func (c ConfigLanguage) NewIdentityManager(name string) identity.Manager {
return identity.NewManager(name)
}
+func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
+ return c.config.C.ContentTypes
+}
+
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any {
switch s {
diff --git a/config/configProvider.go b/config/configProvider.go
index 8f74202ab..ba10d44dd 100644
--- a/config/configProvider.go
+++ b/config/configProvider.go
@@ -41,6 +41,7 @@ type AllProvider interface {
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
+ ContentTypes() ContentTypesProvider
GetConfigSection(string) any
GetConfig() any
CanonifyURLs() bool
@@ -75,6 +76,15 @@ type AllProvider interface {
EnableEmoji() bool
}
+// We cannot import the media package as that would create a circular dependency.
+// This interface defineds a sub set of what media.ContentTypes provides.
+type ContentTypesProvider interface {
+ IsContentSuffix(suffix string) bool
+ IsContentFile(filename string) bool
+ IsIndexContentFile(filename string) bool
+ IsHTMLSuffix(suffix string) bool
+}
+
// Provider provides the configuration settings for Hugo.
type Provider interface {
GetString(key string) string
diff --git a/create/content.go b/create/content.go
index 5c2327532..f7c343d42 100644
--- a/create/content.go
+++ b/create/content.go
@@ -29,8 +29,6 @@ import (
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/hugofs/files"
-
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
@@ -98,7 +96,7 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error
return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath)
}
- if !files.IsContentFile(b.targetPath) {
+ if !h.Conf.ContentTypes().IsContentFile(b.targetPath) {
return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
}
diff --git a/helpers/content.go b/helpers/content.go
index be79ad540..49283d526 100644
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -26,6 +26,7 @@ import (
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/media"
"github.com/spf13/afero"
@@ -135,20 +136,16 @@ func (c *ContentSpec) SanitizeAnchorName(s string) string {
}
func (c *ContentSpec) ResolveMarkup(in string) string {
- if c == nil {
- panic("nil ContentSpec")
- }
in = strings.ToLower(in)
- switch in {
- case "md", "markdown", "mdown":
- return "markdown"
- case "html", "htm":
- return "html"
- default:
- if conv := c.Converters.Get(in); conv != nil {
- return conv.Name()
- }
+
+ if mediaType, found := c.Cfg.ContentTypes().(media.ContentTypes).Types().GetBestMatch(markup.ResolveMarkup(in)); found {
+ return mediaType.SubType
}
+
+ if conv := c.Converters.Get(in); conv != nil {
+ return markup.ResolveMarkup(conv.Name())
+ }
+
return ""
}
@@ -244,7 +241,7 @@ func (c *ContentSpec) TrimShortHTML(input []byte, markup string) []byte {
openingTag := []byte("<p>")
closingTag := []byte("</p>")
- if markup == "asciidocext" {
+ if markup == media.DefaultContentTypes.AsciiDoc.SubType {
openingTag = []byte("<div class=\"paragraph\">\n<p>")
closingTag = []byte("</p>\n</div>")
}
diff --git a/helpers/content_test.go b/helpers/content_test.go
index f1cbfad04..22d468191 100644
--- a/helpers/content_test.go
+++ b/helpers/content_test.go
@@ -41,7 +41,7 @@ func TestTrimShortHTML(t *testing.T) {
{"markdown", []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>"), []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>")},
// Issue 12369
{"markdown", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>")},
- {"asciidocext", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("foo")},
+ {"asciidoc", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("foo")},
}
c := newTestContentSpec(nil)
diff --git a/helpers/general_test.go b/helpers/general_test.go
index 54607d699..7bef1b776 100644
--- a/helpers/general_test.go
+++ b/helpers/general_test.go
@@ -35,9 +35,9 @@ func TestResolveMarkup(t *testing.T) {
{"md", "markdown"},
{"markdown", "markdown"},
{"mdown", "markdown"},
- {"asciidocext", "asciidocext"},
- {"adoc", "asciidocext"},
- {"ad", "asciidocext"},
+ {"asciidocext", "asciidoc"},
+ {"adoc", "asciidoc"},
+ {"ad", "asciidoc"},
{"rst", "rst"},
{"pandoc", "pandoc"},
{"pdc", "pandoc"},
diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go
index a8d231f73..543d741d0 100644
--- a/hugofs/files/classifier.go
+++ b/hugofs/files/classifier.go
@@ -29,57 +29,13 @@ const (
FilenameHugoStatsJSON = "hugo_stats.json"
)
-var (
- // This should be the only list of valid extensions for content files.
- contentFileExtensions = []string{
- "html", "htm",
- "mdown", "markdown", "md",
- "asciidoc", "adoc", "ad",
- "rest", "rst",
- "org",
- "pandoc", "pdc",
- }
-
- contentFileExtensionsSet map[string]bool
-
- htmlFileExtensions = []string{
- "html", "htm",
- }
-