diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go index 993fad5a2..ba367ceb5 100644 --- a/common/hugo/version_current.go +++ b/common/hugo/version_current.go @@ -17,7 +17,7 @@ package hugo // This should be the only one. var CurrentVersion = Version{ Major: 0, - Minor: 146, - PatchLevel: 1, - Suffix: "", + Minor: 148, + PatchLevel: 0, + Suffix: "-DEV", } diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go index 4b3feaa14..b0a2f9fc4 100644 --- a/common/paths/pathparser.go +++ b/common/paths/pathparser.go @@ -124,14 +124,15 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot i if p.posContainerHigh != -1 { return } - mayHaveLang := pp.LanguageIndex != nil + mayHaveLang := p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) mayHaveOutputFormat := component == files.ComponentFolderLayouts - mayHaveKind := mayHaveOutputFormat + mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat + mayHaveLayout := component == files.ComponentFolderLayouts var found bool var high int - if len(p.identifiers) > 0 { + if len(p.identifiersKnown) > 0 { high = lastDot } else { high = len(p.s) @@ -139,9 +140,9 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot i id := types.LowHigh[string]{Low: i + 1, High: high} sid := p.s[id.Low:id.High] - if len(p.identifiers) == 0 { + if len(p.identifiersKnown) == 0 { // The first is always the extension. - p.identifiers = append(p.identifiers, id) + p.identifiersKnown = append(p.identifiersKnown, id) found = true // May also be the output format. @@ -164,8 +165,8 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot i } found = langFound if langFound { - p.identifiers = append(p.identifiers, id) - p.posIdentifierLanguage = len(p.identifiers) - 1 + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierLanguage = len(p.identifiersKnown) - 1 } } @@ -177,28 +178,33 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot i // false positives on the form css.html. if pp.IsOutputFormat(sid, p.Ext()) { found = true - p.identifiers = append(p.identifiers, id) - p.posIdentifierOutputFormat = len(p.identifiers) - 1 + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1 } } if !found && mayHaveKind { if kinds.GetKindMain(sid) != "" { found = true - p.identifiers = append(p.identifiers, id) - p.posIdentifierKind = len(p.identifiers) - 1 + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierKind = len(p.identifiersKnown) - 1 } } if !found && sid == identifierBaseof { found = true - p.identifiers = append(p.identifiers, id) - p.posIdentifierBaseof = len(p.identifiers) - 1 + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierBaseof = len(p.identifiersKnown) - 1 + } + + if !found && mayHaveLayout { + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierLayout = len(p.identifiersKnown) - 1 + found = true } if !found { - p.identifiers = append(p.identifiers, id) - p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1) + p.identifiersUnknown = append(p.identifiersUnknown, id) } } @@ -252,13 +258,13 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { } } - if len(p.identifiers) > 0 { + if len(p.identifiersKnown) > 0 { isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes isContent := isContentComponent && pp.IsContentExt(p.Ext()) - id := p.identifiers[len(p.identifiers)-1] + id := p.identifiersKnown[len(p.identifiersKnown)-1] - if id.High > p.posContainerHigh { - b := p.s[p.posContainerHigh:id.High] + if id.Low > p.posContainerHigh { + b := p.s[p.posContainerHigh : id.Low-1] if isContent { switch b { case "index": @@ -294,6 +300,16 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { } } + if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 { + // myshortcode or myshortcode.html, no layout. + if len(p.identifiersKnown) <= 2 { + p.posIdentifierLayout = -1 + } else { + // First is always the name. + p.posIdentifierLayout-- + } + } + return p, nil } @@ -350,13 +366,14 @@ type Path struct { component string pathType Type - identifiers []types.LowHigh[string] + identifiersKnown []types.LowHigh[string] + identifiersUnknown []types.LowHigh[string] posIdentifierLanguage int posIdentifierOutputFormat int posIdentifierKind int + posIdentifierLayout int posIdentifierBaseof int - identifiersUnknown []int disabled bool trimLeadingSlash bool @@ -388,10 +405,11 @@ func (p *Path) reset() { p.posSectionHigh = -1 p.component = "" p.pathType = 0 - p.identifiers = p.identifiers[:0] + p.identifiersKnown = p.identifiersKnown[:0] p.posIdentifierLanguage = -1 p.posIdentifierOutputFormat = -1 p.posIdentifierKind = -1 + p.posIdentifierLayout = -1 p.posIdentifierBaseof = -1 p.disabled = false p.trimLeadingSlash = false @@ -479,7 +497,7 @@ func (p *Path) Name() string { // Name returns the last element of path without any extension. func (p *Path) NameNoExt() string { if i := p.identifierIndex(0); i != -1 { - return p.s[p.posContainerHigh : p.identifiers[i].Low-1] + return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1] } return p.s[p.posContainerHigh:] } @@ -491,7 +509,7 @@ func (p *Path) NameNoLang() string { return p.Name() } - return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:] + return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:] } // BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension). @@ -510,15 +528,15 @@ func (p *Path) NameNoIdentifier() string { } func (p *Path) nameLowHigh() types.LowHigh[string] { - if len(p.identifiers) > 0 { - lastID := p.identifiers[len(p.identifiers)-1] + if len(p.identifiersKnown) > 0 { + lastID := p.identifiersKnown[len(p.identifiersKnown)-1] if p.posContainerHigh == lastID.Low { // The last identifier is the name. return lastID } return types.LowHigh[string]{ Low: p.posContainerHigh, - High: p.identifiers[len(p.identifiers)-1].Low - 1, + High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1, } } return types.LowHigh[string]{ @@ -566,7 +584,7 @@ func (p *Path) PathNoIdentifier() string { // PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format. func (p *Path) PathBeforeLangAndOutputFormatAndExt() string { - if len(p.identifiers) == 0 { + if len(p.identifiersKnown) == 0 { return p.norm(p.s) } i := p.identifierIndex(0) @@ -582,7 +600,7 @@ func (p *Path) PathBeforeLangAndOutputFormatAndExt() string { return p.norm(p.s) } - id := p.identifiers[i] + id := p.identifiersKnown[i] return p.norm(p.s[:id.Low-1]) } @@ -633,11 +651,11 @@ func (p *Path) BaseNoLeadingSlash() string { } func (p *Path) base(preserveExt, isBundle bool) string { - if len(p.identifiers) == 0 { + if len(p.identifiersKnown) == 0 { return p.norm(p.s) } - if preserveExt && len(p.identifiers) == 1 { + if preserveExt && len(p.identifiersKnown) == 1 { // Preserve extension. return p.norm(p.s) } @@ -659,7 +677,7 @@ func (p *Path) base(preserveExt, isBundle bool) string { } // For txt files etc. we want to preserve the extension. - id := p.identifiers[0] + id := p.identifiersKnown[0] return p.norm(p.s[:high] + p.s[id.Low-1:id.High]) } @@ -676,6 +694,10 @@ func (p *Path) Kind() string { return p.identifierAsString(p.posIdentifierKind) } +func (p *Path) Layout() string { + return p.identifierAsString(p.posIdentifierLayout) +} + func (p *Path) Lang() string { return p.identifierAsString(p.posIdentifierLanguage) } @@ -689,8 +711,8 @@ func (p *Path) Disabled() bool { } func (p *Path) Identifiers() []string { - ids := make([]string, len(p.identifiers)) - for i, id := range p.identifiers { + ids := make([]string, len(p.identifiersKnown)) + for i, id := range p.identifiersKnown { ids[i] = p.s[id.Low:id.High] } return ids @@ -699,7 +721,7 @@ func (p *Path) Identifiers() []string { func (p *Path) IdentifiersUnknown() []string { ids := make([]string, len(p.identifiersUnknown)) for i, id := range p.identifiersUnknown { - ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High] + ids[i] = p.s[id.Low:id.High] } return ids } @@ -724,7 +746,7 @@ func (p *Path) IsContentData() bool { return p.pathType == TypeContentData } -func (p Path) ForBundleType(t Type) *Path { +func (p Path) ForType(t Type) *Path { p.pathType = t return &p } @@ -735,12 +757,12 @@ func (p *Path) identifierAsString(i int) string { return "" } - id := p.identifiers[i] + id := p.identifiersKnown[i] return p.s[id.Low:id.High] } func (p *Path) identifierIndex(i int) int { - if i < 0 || i >= len(p.identifiers) { + if i < 0 || i >= len(p.identifiersKnown) { return -1 } return i diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go index fd1590c73..a6194e756 100644 --- a/common/paths/pathparser_test.go +++ b/common/paths/pathparser_test.go @@ -171,22 +171,25 @@ func TestParse(t *testing.T) { "/a/b.a.b.no.txt", func(c *qt.C, p *Path) { c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt") - c.Assert(p.NameNoIdentifier(), qt.Equals, "b") + c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b") c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt") - c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"}) - c.Assert(p.Base(), qt.Equals, "/a/b.txt") - c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt") + c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt") + c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt") c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt") - c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt") + c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.Ext(), qt.Equals, "txt") - c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b") + c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b") }, }, { "Home branch cundle", "/_index.md", func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) + c.Assert(p.IsBranchBundle(), qt.IsTrue) + c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.Base(), qt.Equals, "/") c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo") c.Assert(p.Path(), qt.Equals, "/_index.md") @@ -206,7 +209,8 @@ func TestParse(t *testing.T) { c.Assert(p.ContainerDir(), qt.Equals, "") c.Assert(p.Dir(), qt.Equals, "/a") c.Assert(p.Ext(), qt.Equals, "md") - c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) c.Assert(p.IsBranchBundle(), qt.IsFalse) c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsLeafBundle(), qt.IsTrue) @@ -228,7 +232,7 @@ func TestParse(t *testing.T) { c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Ext(), qt.Equals, "md") - c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"}) c.Assert(p.IsBranchBundle(), qt.IsFalse) c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsLeafBundle(), qt.IsTrue) @@ -250,7 +254,7 @@ func TestParse(t *testing.T) { c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.Ext(), qt.Equals, "md") - c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"}) c.Assert(p.IsBranchBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsLeafBundle(), qt.IsFalse) @@ -289,7 +293,7 @@ func TestParse(t *testing.T) { func(c *qt.C, p *Path) { c.Assert(p.Base(), qt.Equals, "/a/b/index.txt") c.Assert(p.Ext(), qt.Equals, "txt") - c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) c.Assert(p.IsLeafBundle(), qt.IsFalse) c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index") }, @@ -372,7 +376,7 @@ func TestParse(t *testing.T) { } for _, test := range tests { c.Run(test.name, func(c *qt.C) { - if test.name != "Basic Markdown file" { + if test.name != "Home branch cundle" { // return } test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path)) @@ -401,10 +405,58 @@ func TestParseLayouts(t *testing.T) { "/list.no.html", func(c *qt.C, p *Path) { c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) c.Assert(p.Base(), qt.Equals, "/list.html") c.Assert(p.Lang(), qt.Equals, "no") }, }, + { + "Kind", + "/section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Kind(), qt.Equals, kinds.KindSection) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/section.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout", + "/list.section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/list.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout multiple", + "/maylayout.list.section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "maylayout") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "maylayout"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/maylayout.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout shortcode", + "/_shortcodes/myshort.list.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + }, + }, + { + "Layout baseof", + "/baseof.list.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + }, + }, { "Lang and output format", "/list.no.amp.not.html", @@ -429,6 +481,20 @@ func TestParseLayouts(t *testing.T) { c.Assert(p.OutputFormat(), qt.Equals, "html") }, }, + { + "Shortcode with layout", + "/_shortcodes/myshortcode.list.html", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html") + c.Assert(p.Type(), qt.Equals, TypeShortcode) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "myshortcode"}) + c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode") + c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list") + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.Kind(), qt.Equals, "") + c.Assert(p.OutputFormat(), qt.Equals, "html") + }, + }, { "Sub dir", "/pages/home.html", @@ -445,7 +511,7 @@ func TestParseLayouts(t *testing.T) { "/pages/baseof.list.section.fr.amp.html", func(c *qt.C, p *Path) { c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"}) - c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) c.Assert(p.Kind(), qt.Equals, kinds.KindSection) c.Assert(p.Lang(), qt.Equals, "fr") c.Assert(p.OutputFormat(), qt.Equals, "amp") @@ -501,6 +567,9 @@ func TestParseLayouts(t *testing.T) { for _, test := range tests { c.Run(test.name, func(c *qt.C) { + if test.name != "Baseof" { + // return + } test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path)) }) } diff --git a/common/paths/paths_integration_test.go b/common/paths/paths_integration_test.go index 62d40f527..f5ea3066a 100644 --- a/common/paths/paths_integration_test.go +++ b/common/paths/paths_integration_test.go @@ -78,3 +78,26 @@ disablePathToLower = true b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single") b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single") } + +func TestIssue13596(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +-- content/p1/index.md -- +--- +title: p1 +--- +-- content/p1/a.1.txt -- +-- content/p1/a.2.txt -- +-- layouts/all.html -- +{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|") + b.AssertFileExists("public/p1/a.1.txt", true) + b.AssertFileExists("public/p1/a.2.txt", true) // fails +} diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index e73153a94..d3ee28490 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -800,30 +800,58 @@ func (c *Configs) IsZero() bool { func (c *Configs) Init() error { var languages langs.Languages - defaultContentLanguage := c.Base.DefaultContentLanguage - for k, v := range c.LanguageConfigMap { + + var langKeys []string + var hasEn bool + + const en = "en" + + for k := range c.LanguageConfigMap { + langKeys = append(langKeys, k) + if k == en { + hasEn = true + } + } + + // Sort the LanguageConfigSlice by language weight (if set) or lang. + sort.Slice(langKeys, func(i, j int) bool { + ki := langKeys[i] + kj := langKeys[j] + lki := c.LanguageConfigMap[ki] + lkj := c.LanguageConfigMap[kj] + li := lki.Languages[ki] + lj := lkj.Languages[kj] + if li.Weight != lj.Weight { + return li.Weight < lj.Weight + } + return ki < kj + }) + + // See issue #13646. + defaultConfigLanguageFallback := en + if !hasEn { + // Pick the first one. + defaultConfigLanguageFallback = langKeys[0] + } + + if c.Base.DefaultContentLanguage == "" { + c.Base.DefaultContentLanguage = defaultConfigLanguageFallback + } + + for _, k := range langKeys { + v := c.LanguageConfigMap[k] + if v.DefaultContentLanguage == "" { + v.DefaultContentLanguage = defaultConfigLanguageFallback + } + c.LanguageConfigSlice = append(c.LanguageConfigSlice, v) languageConf := v.Languages[k] - language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf) + language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf) if err != nil { return err } languages = append(languages, language) } - // Sort the sites by language weight (if set) or lang. - sort.Slice(languages, func(i, j int) bool { - li := languages[i] - lj := languages[j] - if li.Weight != lj.Weight { - return li.Weight < lj.Weight - } - return li.Lang < lj.Lang - }) - - for _, l := range languages { - c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang]) - } - // Filter out disabled languages. var n int for _, l := range languages { @@ -836,12 +864,12 @@ func (c *Configs) Init() error { var languagesDefaultFirst langs.Languages for _, l := range languages { - if l.Lang == defaultContentLanguage { + if l.Lang == c.Base.DefaultContentLanguage { languagesDefaultFirst = append(languagesDefaultFirst, l) } } for _, l := range languages { - if l.Lang != defaultContentLanguage { + if l.Lang != c.Base.DefaultContentLanguage { languagesDefaultFirst = append(languagesDefaultFirst, l) } } @@ -927,17 +955,48 @@ func (c Configs) GetByLang(lang string) config.AllProvider { return nil } +func newDefaultConfig() *Config { + return &Config{ + Taxonomies: map[string]string{"tag": "tags", "category": "categories"}, + Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, + RootConfig: RootConfig{ + Environment: hugo.EnvironmentProduction, + TitleCaseStyle: "AP", + PluralizeListTitles: true, + CapitalizeListTitles: true, + StaticDir: []string{"static"}, + SummaryLength: 70, + Timeout: "60s", + + CommonDirs: config.CommonDirs{ + ArcheTypeDir: "archetypes", + ContentDir: "content", + ResourceDir: "resources", + PublishDir: "public", + ThemesDir: "themes", + AssetDir: "assets", + LayoutDir: "layouts", + I18nDir: "i18n", + DataDir: "data", + }, + }, + } +} + // fromLoadConfigResult creates a new Config from res. func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) { if !res.Cfg.IsSet("languages") { // We need at least one lang := res.Cfg.GetString("defaultContentLanguage") + if lang == "" { + lang = "en" + } res.Cfg.Set("languages", maps.Params{lang: maps.Params{}}) } bcfg := res.BaseConfig cfg := res.Cfg - all := &Config{} + all := newDefaultConfig() err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil) if err != nil { @@ -947,6 +1006,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon langConfigMap := make(map[string]*Config) languagesConfig := cfg.GetStringMap("languages") + var isMultihost bool if err := all.CompileConfig(logger); err != nil { diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go index cae04ba85..033947203 100644 --- a/config/allconfig/allconfig_integration_test.go +++ b/config/allconfig/allconfig_integration_test.go @@ -5,6 +5,7 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/media" @@ -234,3 +235,62 @@ baseURL = "https://example.com" b.Assert(c.IsContentFile("foo.md"), qt.Equals, true) b.Assert(len(s), qt.Equals, 6) } + +func TestMergeDeep(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +theme = ["theme1", "theme2"] +_merge = "deep" +-- themes/theme1/hugo.toml -- +[sitemap] +filename = 'mysitemap.xml' +[services] +[services.googleAnalytics] +id = 'foo bar' +[taxonomies] + foo = 'bars' +-- themes/theme2/config/_default/hugo.toml -- +[taxonomies] + bar = 'baz' +-- layouts/home.html -- +GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}. + +` + + b := hugolib.Test(t, files) + + conf := b.H.Configs + base := conf.Base + + b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction) + b.Assert(base.BaseURL, qt.Equals, "https://example.com") + b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml") + b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"}) + + b.AssertFileContent("public/index.html", "GA ID: foo bar.") +} + +func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[languages] +[languages.nn] +weight = 20 +[languages.sv] +weight = 10 +[languages.sv.taxonomies] + tag = "taggar" +-- layouts/all.html -- +All. +` + + b := hugolib.Test(t, files) + + b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv") +} diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index 0bf8508d9..1cfa1afc4 100644 --- a/config/allconfig/alldecoders.go +++ b/config/allconfig/alldecoders.go @@ -249,14 +249,18 @@ var allDecoderSetups = map[string]decodeWeight{ key: "sitemap", decode: func(d decodeWeight, p decodeConfig) error { var err error - p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key)) + if p.p.IsSet(d.key) { + p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key)) + } return err }, }, "taxonomies": { key: "taxonomies", decode: func(d decodeWeight, p decodeConfig) error { - p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + if p.p.IsSet(d.key) { + p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + } return nil }, }, @@ -306,15 +310,17 @@ var allDecoderSetups = map[string]decodeWeight{ } // Validate defaultContentLanguage. - var found bool - for lang := range p.c.Languages { - if lang == p.c.DefaultContentLanguage { - found = true - break + if p.c.DefaultContentLanguage != "" { + var found bool + for lang := range p.c.Languages { + if lang == p.c.DefaultContentLanguage { + found = true + break + } + } + if !found { + return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage) } - } - if !found { - return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage) } return nil diff --git a/config/allconfig/load.go b/config/allconfig/load.go index f224009ac..2d9185f6f 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -159,63 +159,9 @@ func (l configLoader) applyConfigAliases() error { func (l configLoader) applyDefaultConfig() error { defaultSettings := maps.Params{ - "baseURL": "", - "cleanDestinationDir": false, - "watch": false, - "contentDir": "content", - "resourceDir": "resources", - "publishDir": "public", - "publishDirOrig": "public", - "themesDir": "themes", - "assetDir": "assets", - "layoutDir": "layouts", - "i18nDir": "i18n", - "dataDir": "data", - "archetypeDir": "archetypes", - "configDir": "config", - "staticDir": "static", - "buildDrafts": false, - "buildFuture": false, - "buildExpired": false, - "params": maps.Params{}, - "environment": hugo.EnvironmentProduction, - "uglyURLs": false, - "verbose": false, - "ignoreCache": false, - "canonifyURLs": false, - "relativeURLs": false, - "removePathAccents": false, - "titleCaseStyle": "AP", - "taxonomies": maps.Params{"tag": "tags", "category": "categories"}, - "permalinks": maps.Params{}, - "sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"}, - "menus": maps.Params{}, - "disableLiveReload": false, - "pluralizeListTitles": true, - "capitalizeListTitles": true, - "forceSyncStatic": false, - "footnoteAnchorPrefix": "", - "footnoteReturnLinkContents": "", - "newContentEditor": "", - "paginate": 0, // Moved into the paginator struct in Hugo v0.128.0. - "paginatePath": "", // Moved into the paginator struct in Hugo v0.128.0. - "summaryLength": 70, - "rssLimit": -1, - "sectionPagesMenu": "", - "disablePathToLower": false, - "hasCJKLanguage": false, - "enableEmoji": false, - "defaultContentLanguage": "en", - "defaultContentLanguageInSubdir": false, - "enableMissingTranslationPlaceholders": false, - "enableGitInfo": false, - "ignoreFiles": make([]string, 0), - "disableAliases": false, - "debug": false, - "disableFastRender": false, - "timeout": "30s", - "timeZone": "", - "enableInlineShortcodes": false, + // These dirs are used early/before we build the config struct. + "themesDir": "themes", + "configDir": "config", } l.cfg.SetDefaults(defaultSettings) diff --git a/config/commonConfig.go b/config/commonConfig.go index 3dfd9b409..947078672 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -17,6 +17,7 @@ import ( "fmt" "net/http" "regexp" + "slices" "sort" "strings" @@ -28,7 +29,6 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/mitchellh/mapstructure" "github.com/spf13/cast" - "slices" ) type BaseConfig struct { diff --git a/config/configProvider.go b/config/configProvider.go index 5bda2c55a..c21342dce 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -76,7 +76,7 @@ type AllProvider interface { } // We cannot import the media package as that would create a circular dependency. -// This interface defineds a sub set of what media.ContentTypes provides. +// This interface defines a subset of what media.ContentTypes provides. type ContentTypesProvider interface { IsContentSuffix(suffix string) bool IsContentFile(filename string) bool diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go index f302244d4..f9d5e1a6e 100644 --- a/config/services/servicesConfig.go +++ b/config/services/servicesConfig.go @@ -101,6 +101,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) { if c.RSS.Limit == 0 { c.RSS.Limit = cfg.GetInt(rssLimitKey) + if c.RSS.Limit == 0 { + c.RSS.Limit = -1 + } } return diff --git a/create/skeletons/theme/layouts/single.html b/create/skeletons/theme/layouts/page.html similarity index 100% rename from create/skeletons/theme/layouts/single.html rename to create/skeletons/theme/layouts/page.html diff --git a/create/skeletons/theme/layouts/list.html b/create/skeletons/theme/layouts/section.html similarity index 100% rename from create/skeletons/theme/layouts/list.html rename to create/skeletons/theme/layouts/section.html diff --git a/docs/content/en/configuration/introduction.md b/docs/content/en/configuration/introduction.md index 121a483c4..8f8ad4c1e 100644 --- a/docs/content/en/configuration/introduction.md +++ b/docs/content/en/configuration/introduction.md @@ -249,7 +249,7 @@ HUGO_FILE_LOG_FORMAT HUGO_MEMORYLIMIT : {{< new-in 0.123.0 />}} -: (`int`) The maximum amount of system memory, in gigabytes, that Hugo can use while rendering your site. Default is 25% of total system memory. Note that The `HUGO_MEMORYLIMIT` is a “best effort” setting. Don't expect Hugo to build a million pages with only 1 GB memory. You can get more information about how this behaves during the build by building with `hugo --logLevel info` and look for the `dynacache` label. +: (`int`) The maximum amount of system memory, in gigabytes, that Hugo can use while rendering your site. Default is 25% of total system memory. Note that `HUGO_MEMORYLIMIT` is a "best effort" setting. Don't expect Hugo to build a million pages with only 1 GB of memory. You can get more information about how this behaves during the build by building with `hugo --logLevel info` and look for the `dynacache` label. HUGO_NUMWORKERMULTIPLIER : (`int`) The number of workers used in parallel processing. Default is the number of logical CPUs. diff --git a/docs/content/en/content-management/menus.md b/docs/content/en/content-management/menus.md index ab1bcbfa1..6d01173dc 100644 --- a/docs/content/en/content-management/menus.md +++ b/docs/content/en/content-management/menus.md @@ -11,7 +11,7 @@ aliases: [/extras/menus/] To create a menu for your site: 1. Define the menu entries -1. [Localize] each entry +1. [Localize](multilingual/#menus) each entry 1. Render the menu with a [template] Create multiple menus, either flat or nested. For example, create a main menu for the header, and a separate menu for the footer. diff --git a/docs/content/en/contribute/documentation.md b/docs/content/en/contribute/documentation.md index 1d185d21d..68129912a 100644 --- a/docs/content/en/contribute/documentation.md +++ b/docs/content/en/contribute/documentation.md @@ -112,7 +112,7 @@ Yes → Hugo is fast. ### Function and method descriptions -Start descriptions in the functions and methods sections with "Returns", of for booelan values, "Reports whether". +Start descriptions in the functions and methods sections with "Returns", or for boolean values, "Reports whether". ### File paths and names diff --git a/docs/content/en/functions/collections/Where.md b/docs/content/en/functions/collections/Where.md index 1df84afc4..84fd1d21e 100644 --- a/docs/content/en/functions/collections/Where.md +++ b/docs/content/en/functions/collections/Where.md @@ -87,7 +87,6 @@ Use any of the following logical operators: : (`bool`) Reports whether the given field value (a slice) contains one or more elements in common with `VALUE`. See [details](/functions/collections/intersect). `like` -: {{< new-in 0.116.0 />}} : (`bool`) Reports whether the given field value matches the [regular expression](g) specified in `VALUE`. Use the `like` operator to compare `string` values. The `like` operator returns `false` when comparing other data types to the regular expression. > [!note] @@ -167,8 +166,6 @@ For example, to return a collection of pages where any of the terms in the "genr ## Regular expression comparison -{{< new-in 0.116.0 />}} - To return a collection of pages where the "author" page parameter begins with either "victor" or "Victor": ```go-html-template diff --git a/docs/content/en/functions/css/Sass.md b/docs/content/en/functions/css/Sass.md index 1d5487130..03a4c7451 100644 --- a/docs/content/en/functions/css/Sass.md +++ b/docs/content/en/functions/css/Sass.md @@ -12,13 +12,66 @@ params: {{< new-in 0.128.0 />}} -```go-html-template +Transpile Sass to CSS using the LibSass transpiler included in Hugo's extended and extended/deploy editions, or [install Dart Sass](#dart-sass) to use the latest features of the Sass language. + +Sass has two forms of syntax: [SCSS] and [indented]. Hugo supports both. + +[scss]: https://sass-lang.com/documentation/syntax#scss +[indented]: https://sass-lang.com/documentation/syntax#the-indented-syntax + +## Options + +enableSourceMap +: (`bool`) Whether to generate a source map. Default is `false`. + +includePaths +: (`slice`) A slice of paths, relative to the project root, that the transpiler will use when resolving `@use` and `@import` statements. + +outputStyle +: (`string`) The output style of the resulting CSS. With LibSass, one of `nested` (default), `expanded`, `compact`, or `compressed`. With Dart Sass, either `expanded` (default) or `compressed`. + +precision +: (`int`) The precision of floating point math. Applicable to LibSass. Default is `8`. + +silenceDeprecations +: {{< new-in 0.139.0 />}} +: (`slice`) A slice of deprecation IDs to silence. IDs are enclosed in brackets within Dart Sass warning messages (e.g., `import` in `WARN Dart Sass: DEPRECATED [import]`). Applicable to Dart Sass. Default is `false`. + +silenceDependencyDeprecations +: {{< new-in 0.146.0 />}} +: (`bool`) Whether to silence deprecation warnings from dependencies, where a dependency is considered any file transitively imported through a load path. This does not apply to `@warn` or `@debug` rules.Default is `false`. + +sourceMapIncludeSources +: (`bool`) Whether to embed sources in the generated source map. Applicable to Dart Sass. Default is `false`. + +targetPath +: (`string`) The publish path for the transformed resource, relative to the[`publishDir`]. If unset, the target path defaults to the asset's original path with a `.css` extension. + +transpiler +: (`string`) The transpiler to use, either `libsass` or `dartsass`. Hugo's extended and extended/deploy editions include the LibSass transpiler. To use the Dart Sass transpiler, see the [installation instructions](#dart-sass). Default is `libsass`. + +vars +: (`map`) A map of key-value pairs that will be available in the `hugo:vars` namespace. Useful for [initializing Sass variables from Hugo templates](https://discourse.gohugo.io/t/42053/). + + ```scss + // LibSass + @import "hugo:vars"; + + // Dart Sass + @use "hugo:vars" as v; + ``` + +## Example + +```go-html-template {copy=true} {{ with resources.Get "sass/main.scss" }} {{ $opts := dict "enableSourceMap" (not hugo.IsProduction) "outputStyle" (cond hugo.IsProduction "compressed" "expanded") "targetPath" "css/main.css" - "transpiler" "libsass" + "transpiler" "dartsass" + "vars" site.Params.styles + "includePaths" (slice "node_modules/bootstrap/scss") }} {{ with . | toCSS $opts }} {{ if hugo.IsProduction }} @@ -32,63 +85,6 @@ params: {{ end }} ``` -Transpile Sass to CSS using the LibSass transpiler included in Hugo's extended and extended/deploy editions, or [install Dart Sass](#dart-sass) to use the latest features of the Sass language. - -Sass has two forms of syntax: [SCSS] and [indented]. Hugo supports both. - -[scss]: https://sass-lang.com/documentation/syntax#scss -[indented]: https://sass-lang.com/documentation/syntax#the-indented-syntax - -## Options - -transpiler -: (`string`) The transpiler to use, either `libsass` (default) or `dartsass`. Hugo's extended and extended/deploy editions include the LibSass transpiler. To use the Dart Sass transpiler, see the [installation instructions](#dart-sass) below. - -targetPath -: (`string`) If not set, the transformed resource's target path will be the original path of the asset file with its extension replaced by `.css`. - -vars -: (`map`) A map of key-value pairs that will be available in the `hugo:vars` namespace. Useful for [initializing Sass variables from Hugo templates](https://discourse.gohugo.io/t/42053/). - -```scss -// LibSass -@import "hugo:vars"; - -// Dart Sass -@use "hugo:vars" as v; -``` - -outputStyle -: (`string`) Output styles available to LibSass include `nested` (default), `expanded`, `compact`, and `compressed`. Output styles available to Dart Sass include `expanded` (default) and `compressed`. - -precision -: (`int`) Precision of floating point math. Not applicable to Dart Sass. - -enableSourceMap -: (`bool`) Whether to generate a source map. Default is `false`. - -sourceMapIncludeSources -: (`bool`) Whether to embed sources in the generated source map. Not applicable to LibSass. Default is `false`. - -includePaths -: (`slice`) A slice of paths, relative to the project root, that the transpiler will use when resolving `@use` and `@import` statements. - -```go-html-template -{{ $opts := dict - "transpiler" "dartsass" - "targetPath" "css/style.css" - "vars" site.Params.styles - "enableSourceMap" (not hugo.IsProduction) - "includePaths" (slice "node_modules/bootstrap/scss") -}} -{{ with resources.Get "sass/main.scss" | toCSS $opts | minify | fingerprint }} - -{{ end }} -``` - -silenceDeprecations -: (`slice`) {{< new-in 0.139.0 />}} A slice of deprecation IDs to silence. The deprecation IDs are printed to in the warning message, e.g "import" in `WARN Dart Sass: DEPRECATED [import] ...`. This is for Dart Sass only. - ## Dart Sass Hugo's extended and extended/deploy editions include [LibSass] to transpile Sass to CSS. In 2020, the Sass team deprecated LibSass in favor of [Dart Sass]. @@ -121,6 +117,9 @@ You may also install [prebuilt binaries] for Linux, macOS, and Windows. Run `hugo env` to list the active transpilers. +> [!note] +> If you build Hugo from source and run `mage test -v`, the test will fail if you install Dart Sass as a Snap package. This is due to the Snap package's strict confinement model. + ### Installing in a production environment For [CI/CD](g) deployments (e.g., GitHub Pages, GitLab Pages, Netlify, etc.) you must edit the workflow to install Dart Sass before Hugo builds the site[^2]. Some providers allow you to use one of the package managers above, or you can download and extract one of the prebuilt binaries. @@ -136,8 +135,6 @@ To install Dart Sass for your builds on GitHub Pages, add this step to the GitHu run: sudo snap install dart-sass ``` -If you are using GitHub Pages for the first time with your repository, GitHub provides a [starter workflow] for Hugo that includes Dart Sass. This is the simplest way to get started. - #### GitLab Pages To install Dart Sass for your builds on GitLab Pages, the `.gitlab-ci.yml` file should look something like this: @@ -194,34 +191,6 @@ command = """\ """ ``` -### Example - -To transpile with Dart Sass, set `transpiler` to `dartsass` in the options map passed to `css.Sass`. For example: - -```go-html-template -{{ with resources.Get "sass/main.scss" }} - {{ $opts := dict - "enableSourceMap" (not hugo.IsProduction) - "outputStyle" (cond hugo.IsProduction "compressed" "expanded") - "targetPath" "css/main.css" - "transpiler" "dartsass" - }} - {{ with . | toCSS $opts }} - {{ if hugo.IsProduction }} - {{ with . | fingerprint }} - - {{ end }} - {{ else }} - - {{ end }} - {{ end }} -{{ end }} -``` - -### Miscellaneous - -If you build Hugo from source and run `mage test -v`, the test will fail if you install Dart Sass as a Snap package. This is due to the Snap package's strict confinement model. - [brew.sh]: https://brew.sh/ [chocolatey.org]: https://community.chocolatey.org/packages/sass [dart sass]: https://sass-lang.com/dart-sass @@ -232,3 +201,4 @@ If you build Hugo from source and run `mage test -v`, the test will fail if you [snap package]: /installation/linux/#snap [snapcraft.io]: https://snapcraft.io/dart-sass [starter workflow]: https://github.com/actions/starter-workflows/blob/main/pages/hugo.yml +[`publishDir`]: /configuration/all/#publishdir diff --git a/docs/content/en/functions/go-template/template.md b/docs/content/en/functions/go-template/template.md index dac1fa3be..053cfcc22 100644 --- a/docs/content/en/functions/go-template/template.md +++ b/docs/content/en/functions/go-template/template.md @@ -10,7 +10,18 @@ params: signatures: ['template NAME [CONTEXT]'] --- -Use the `template` function to execute [embedded templates]. For example: +Use the `template` function to execute any of these [embedded templates](g): + +- [`disqus.html`] +- [`google_analytics.html`] +- [`opengraph.html`] +- [`pagination.html`] +- [`schema.html`] +- [`twitter_cards.html`] + + + +For example: ```go-html-template {{ range (.Paginate .Pages).Pages }} @@ -39,8 +50,21 @@ The example above can be rewritten using an [inline partial] template: {{ end }} ``` +The key distinctions between the preceding two examples are: + +1. Inline partials are globally scoped. That means that an inline partial defined in _one_ template may be called from _any_ template. +2. Leveraging the [`partialCached`] function when calling an inline partial allows for performance optimization through result caching. +3. An inline partial can [`return`] a value of any data type instead of rendering a string. + {{% include "/_common/functions/go-template/text-template.md" %}} +[`disqus.html`]: /templates/embedded/#disqus +[`google_analytics.html`]: /templates/embedded/#google-analytics +[`opengraph.html`]: /templates/embedded/#open-graph +[`pagination.html`]: /templates/embedded/#pagination +[`partialCached`]: /functions/partials/includecached/ [`partial`]: /functions/partials/include/ +[`return`]: /functions/go-template/return/ +[`schema.html`]: /templates/embedded/#schema +[`twitter_cards.html`]: /templates/embedded/#x-twitter-cards [inline partial]: /templates/partial/#inline-partials -[embedded templates]: /templates/embedded/ diff --git a/docs/content/en/functions/images/Text.md b/docs/content/en/functions/images/Text.md index 94cdb4e9d..8f7e730ba 100644 --- a/docs/content/en/functions/images/Text.md +++ b/docs/content/en/functions/images/Text.md @@ -18,6 +18,9 @@ alignx : {{< new-in 0.141.0 />}} : (`string`) The horizontal alignment of the text relative to the horizontal offset, one of `left`, `center`, or `right`. Default is `left`. +aligny +: (`string`) The vertical alignment of the text relative to the vertical offset, one of `top`, `center`, or `bottom`. Default is `top`. + color : (`string`) The font color, either a 3-digit or 6-digit hexadecimal color code. Default is `#ffffff` (white). diff --git a/docs/content/en/functions/templates/Current.md b/docs/content/en/functions/templates/Current.md new file mode 100644 index 000000000..805aeec05 --- /dev/null +++ b/docs/content/en/functions/templates/Current.md @@ -0,0 +1,155 @@ +--- +title: templates.Current +description: Returns information about the currently executing template. +categories: [] +keywords: [] +params: + functions_and_methods: + aliases: [] + returnType: tpl.CurrentTemplateInfo + signatures: [templates.Current] +--- + +> [!note] +> This function is experimental and subject to change. + +{{< new-in 0.146.0 />}} + +The `templates.Current` function provides introspection capabilities, allowing you to access details about the currently executing templates. This is useful for debugging complex template hierarchies and understanding the flow of execution during rendering. + +## Methods + +Ancestors +: (`tpl.CurrentTemplateInfos`) Returns a slice containing information about each template in the current execution chain, starting from the parent of the current template and going up towards the initial template called. It excludes any base template applied via `define` and `block`. You can chain the `Reverse` method to this result to get the slice in chronological execution order. + +Base +: (`tpl.CurrentTemplateInfoCommonOps`) Returns an object representing the base template that was applied to the current template, if any. This may be `nil`. + +Filename +: (`string`) Returns the absolute path of the current template. This will be empty for embedded templates. + +Name +: (`string`) Returns the name of the current template. This is usually the path relative to the layouts directory. + +Parent +: (`tpl.CurrentTemplateInfo`) Returns an object representing the parent of the current template, if any. This may be `nil`. + +## Examples + +The examples below help visualize template execution and require a `debug` parameter set to `true` in your site configuration: + +{{< code-toggle file=hugo >}} +[params] +debug = true +{{< /code-toggle >}} + +### Boundaries + +To visually mark where a template begins and ends execution: + +```go-html-template {file="layouts/_default/single.html"} +{{ define "main" }} + {{ if site.Params.debug }} +
[entering {{ templates.Current.Filename }}]
+ {{ end }} + +

{{ .Title }}

+ {{ .Content }} + + {{ if site.Params.debug }} +
[leaving {{ templates.Current.Filename }}]
+ {{ end }} +{{ end }} +``` + +### Call stack + +To display the chain of templates that led to the current one, create a partial template that iterates through its ancestors: + +```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} +{{ with templates.Current }} +
+ {{ range .Ancestors }} + {{ .Filename }}
+ {{ with .Base }} + {{ .Filename }}
+ {{ end }} + {{ end }} +
+{{ end }} +``` + +Then call the partial from any template: + +```go-html-template {file="layouts/partials/footer/copyright.html" copy=true} +{{ if site.Params.debug }} + {{ partial "template-call-stack.html" . }} +{{ end }} +``` + +The rendered template stack would look something like this: + +```text +/home/user/project/layouts/partials/footer/copyright.html +/home/user/project/themes/foo/layouts/partials/footer.html +/home/user/project/layouts/_default/single.html +/home/user/project/themes/foo/layouts/_default/baseof.html +``` + +To reverse the order of the entries, chain the `Reverse` method to the `Ancestors` method: + +```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} +{{ with templates.Current }} +
+ {{ range .Ancestors.Reverse }} + {{ with .Base }} + {{ .Filename }}
+ {{ end }} + {{ .Filename }}
+ {{ end }} +
+{{ end }} +``` + +### VS Code + +To render links that, when clicked, will open the template in Microsoft Visual Studio Code, create a partial template with anchor elements that use the `vscode` URI scheme: + +```go-html-template {file="layouts/partials/template-open-in-vs-code.html" copy=true} +{{ with templates.Current.Parent }} +
+ {{ .Name }} + {{ with .Base }} + {{ .Name }} + {{ end }} +
+{{ end }} +``` + +Then call the partial from any template: + +```go-html-template {file="layouts/_default/single.html" copy=true} +{{ define "main" }} +

{{ .Title }}

+ {{ .Content }} + + {{ if site.Params.debug }} + {{ partial "template-open-in-vs-code.html" . }} + {{ end }} +{{ end }} +``` + +Use the same approach to render the entire call stack as links: + +```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} +{{ with templates.Current }} +
+ {{ range .Ancestors }} + {{ .Filename }}
+ {{ with .Base }} + {{ .Filename }}
+ {{ end }} + {{ end }} +
+{{ end }} +``` diff --git a/docs/content/en/functions/time/In.md b/docs/content/en/functions/time/In.md new file mode 100644 index 000000000..821eb99b7 --- /dev/null +++ b/docs/content/en/functions/time/In.md @@ -0,0 +1,30 @@ +--- +title: time.In +description: Returns the given date/time as represented in the specified IANA time zone. +categories: [] +keywords: [] +params: + functions_and_methods: + aliases: [] + returnType: time.Time + signatures: [time.In TIMEZONE INPUT] +--- + +{{< new-in 0.146.0 />}} + +The `time.In` function returns the given date/time as represented in the specified [IANA](g) time zone. + +- If the time zone is an empty string or `UTC`, the time is returned in [UTC](g). +- If the time zone is `Local`, the time is returned in the system's local time zone. +- Otherwise, the time zone must be a valid IANA [time zone name]. + +[time zone name]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List + +```go-html-template +{{ $layout := "2006-01-02T15:04:05-07:00" }} +{{ $t := time.AsTime "2025-03-31T14:45:00-00:00" }} + +{{ $t | time.In "America/Denver" | time.Format $layout }} → 2025-03-31T08:45:00-06:00 +{{ $t | time.In "Australia/Adelaide" | time.Format $layout }} → 2025-04-01T01:15:00+10:30 +{{ $t | time.In "Europe/Oslo" | time.Format $layout }} → 2025-03-31T16:45:00+02:00 +``` diff --git a/docs/content/en/functions/transform/Unmarshal.md b/docs/content/en/functions/transform/Unmarshal.md index d159122f5..93168294c 100644 --- a/docs/content/en/functions/transform/Unmarshal.md +++ b/docs/content/en/functions/transform/Unmarshal.md @@ -114,12 +114,14 @@ A remote resource is a file on a remote server, accessible via HTTP or HTTPS. > > `{{ $data = .Content | transform.Unmarshal }}` -## Options +## Working with CSV + +### Options When unmarshaling a CSV file, provide an optional map of options. delimiter -: (`string`) The delimiter used, default is `,`. +: (`string`) The delimiter used. Default is `,`. comment : (`string`) The comment character used in the CSV. If set, lines beginning with the comment character without preceding whitespace are ignored. @@ -128,8 +130,85 @@ lazyQuotes : {{< new-in 0.122.0 />}} : (`bool`) Whether to allow a quote in an unquoted field, or to allow a non-doubled quote in a quoted field. Default is `false`. +targetType +: {{< new-in 0.146.7 />}} +: (`string`) The target data type, either `slice` or `map`. Default is `slice`. + +### Examples + +The examples below use this CSV file: + +```csv +"name","type","breed","age" +"Spot","dog","Collie",3 +"Rover","dog","Boxer",5 +"Felix","cat","Calico",7 +``` + +To render an HTML table from a CSV file: + ```go-html-template -{{ $csv := "a;b;c" | transform.Unmarshal (dict "delimiter" ";") }} +{{ $data := slice }} +{{ $file := "pets.csv" }} +{{ with or (.Resources.Get $file) (resources.Get $file) }} + {{ $opts := dict "targetType" "slice" }} + {{ $data = transform.Unmarshal $opts . }} +{{ end }} + +{{ with $data }} + + + + {{ range index . 0 }} + + {{ end }} + + + + {{ range . | after 1 }} + + {{ range . }} + + {{ end }} + + {{ end }} + +
{{ . }}
{{ . }}
+{{ end }} +``` + +To extract a subset of the data, or to sort the data, unmarshal to a map instead of a slice: + +```go-html-template +{{ $data := slice }} +{{ $file := "pets.csv" }} +{{ with or (.Resources.Get $file) (resources.Get $file) }} + {{ $opts := dict "targetType" "map" }} + {{ $data = transform.Unmarshal $opts . }} +{{ end }} + +{{ with sort (where $data "type" "dog") "name" "asc" }} + + + + + + + + + + + {{ range . }} + + + + + + + {{ end }} + +
nametypebreedage
{{ .name }}{{ .type }}{{ .breed }}{{ .age }}
+{{ end }} ``` ## Working with XML diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/index.md b/docs/content/en/host-and-deploy/host-on-github-pages/index.md index 4c00fbc8e..7c3201099 100644 --- a/docs/content/en/host-and-deploy/host-on-github-pages/index.md +++ b/docs/content/en/host-and-deploy/host-on-github-pages/index.md @@ -136,6 +136,8 @@ jobs: key: hugo-${{ github.run_id }} restore-keys: hugo- + - name: Configure Git + run: git config core.quotepath false - name: Build with Hugo run: | hugo \ diff --git a/docs/content/en/host-and-deploy/host-on-gitlab-pages.md b/docs/content/en/host-and-deploy/host-on-gitlab-pages.md index 4b308cc19..4750b0ff3 100644 --- a/docs/content/en/host-and-deploy/host-on-gitlab-pages.md +++ b/docs/content/en/host-and-deploy/host-on-gitlab-pages.md @@ -23,15 +23,15 @@ Define your [CI/CD](g) jobs by creating a `.gitlab-ci.yml` file in the root of y ```yaml {file=".gitlab-ci.yml" copy=true} variables: - DART_SASS_VERSION: 1.85.0 + DART_SASS_VERSION: 1.87.0 GIT_DEPTH: 0 GIT_STRATEGY: clone GIT_SUBMODULE_STRATEGY: recursive - HUGO_VERSION: 0.144.2 - NODE_VERSION: 23.x + HUGO_VERSION: 0.146.7 + NODE_VERSION: 22.x TZ: America/Los_Angeles image: - name: golang:1.23.4-bookworm + name: golang:1.24.2-bookworm pages: script: @@ -53,6 +53,8 @@ pages: - apt-get install -y nodejs # Install Node.js dependencies - "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" + # Configure Git + - git config core.quotepath false # Build - hugo --gc --minify --baseURL ${CI_PAGES_URL} # Compress diff --git a/docs/content/en/host-and-deploy/host-on-netlify/index.md b/docs/content/en/host-and-deploy/host-on-netlify/index.md index f3601331a..4c89a6c1e 100644 --- a/docs/content/en/host-and-deploy/host-on-netlify/index.md +++ b/docs/content/en/host-and-deploy/host-on-netlify/index.md @@ -113,21 +113,23 @@ Create a new file named netlify.toml in the root of your project directory. In i ```toml {file="netlify.toml"} [build.environment] -HUGO_VERSION = "0.144.2" +GO_VERSION = "1.24" +HUGO_VERSION = "0.146.7" NODE_VERSION = "22" TZ = "America/Los_Angeles" [build] publish = "public" -command = "hugo --gc --minify" +command = "git config core.quotepath false && hugo --gc --minify" ``` If your site requires Dart Sass to transpile Sass to CSS, the configuration file should look something like this: ```toml {file="netlify.toml"} [build.environment] -HUGO_VERSION = "0.144.2" -DART_SASS_VERSION = "1.85.0" +DART_SASS_VERSION = "1.87.0" +GO_VERSION = "1.24" +HUGO_VERSION = "0.146.7" NODE_VERSION = "22" TZ = "America/Los_Angeles" @@ -138,6 +140,7 @@ command = """\ tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ rm dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ export PATH=/opt/build/repo/dart-sass:$PATH && \ + git config core.quotepath false && \ hugo --gc --minify \ """ ``` diff --git a/docs/content/en/installation/linux.md b/docs/content/en/installation/linux.md index 731cfce4c..591bf0818 100644 --- a/docs/content/en/installation/linux.md +++ b/docs/content/en/installation/linux.md @@ -30,17 +30,33 @@ To install the extended edition of Hugo: sudo snap install hugo ``` -To enable or revoke access to removable media: +To control automatic updates: ```sh +# disable automatic updates +sudo snap refresh --hold hugo + +# enable automatic updates +sudo snap refresh --unhold hugo +``` + +To control access to removable media: + +```sh +# allow access sudo snap connect hugo:removable-media + +# revoke access sudo snap disconnect hugo:removable-media ``` -To enable or revoke access to SSH keys: +To control access to SSH keys: ```sh +# allow access sudo snap connect hugo:ssh-keys + +# revoke access sudo snap disconnect hugo:ssh-keys ``` diff --git a/docs/content/en/news/_content.gotmpl b/docs/content/en/news/_content.gotmpl index f979c9adc..af3cf47ed 100644 --- a/docs/content/en/news/_content.gotmpl +++ b/docs/content/en/news/_content.gotmpl @@ -23,7 +23,8 @@ "dates" $dates "kind" "page" "params" $params - "path" .name + "path" (strings.Replace .name "." "-") + "slug" .name "title" (printf "Release %s" .name) }} {{ $.AddPage $page }} diff --git a/docs/content/en/quick-reference/glossary/iana.md b/docs/content/en/quick-reference/glossary/iana.md new file mode 100644 index 000000000..89497f76a --- /dev/null +++ b/docs/content/en/quick-reference/glossary/iana.md @@ -0,0 +1,6 @@ +--- +title: IANA +reference: https://www.iana.org/about +--- + +_IANA_ is an abbreviation for the Internet Assigned Numbers Authority, a non-profit organization that manages the allocation of global IP addresses, autonomous system numbers, DNS root zone, media types, and other Internet Protocol-related resources. diff --git a/docs/content/en/quick-reference/glossary/utc.md b/docs/content/en/quick-reference/glossary/utc.md new file mode 100644 index 000000000..a4627be5a --- /dev/null +++ b/docs/content/en/quick-reference/glossary/utc.md @@ -0,0 +1,6 @@ +--- +title: UTC +reference: https://en.wikipedia.org/wiki/Coordinated_Universal_Time +--- + +_UTC_ is an abbreviation for Coordinated Universal Time, the primary time standard used worldwide to regulate clocks and time. It is the basis for civil time and time zones across the globe. diff --git a/docs/content/en/shortcodes/ref.md b/docs/content/en/shortcodes/ref.md index 2f821254c..a52c2bf6e 100755 --- a/docs/content/en/shortcodes/ref.md +++ b/docs/content/en/shortcodes/ref.md @@ -10,7 +10,7 @@ keywords: [] > To override Hugo's embedded `ref` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. > [!note] -> When working with the Markdown [content format], this shortcode has become largely redundant. Its functionality is now primarily handled by [link render hooks], specifically the embedded one provided by Hugo. This hook effectively addresses all the use cases previously covered by this shortcode. +> When working with Markdown, this shortcode is obsolete. Instead, use a [link render hook] that resolves the link destination using the `GetPage` method on the `Page` object. You can either create your own, or simply enable the [embedded link render hook]. The embedded link render hook is automatically enabled for multilingual single-host projects. ## Usage @@ -56,6 +56,7 @@ Rendered: {{% include "_common/ref-and-relref-error-handling.md" %}} [content format]: /content-management/formats/ -[link render hooks]: /render-hooks/images/#default +[embedded link render hook]: /render-hooks/links/#default +[link render hook]: /render-hooks/links/ [Markdown notation]: /content-management/shortcodes/#notation -[source code]: {{% eturl ref %}} +[source code]: {{% eturl relref %}} diff --git a/docs/content/en/shortcodes/relref.md b/docs/content/en/shortcodes/relref.md index 5b413b87e..219eae81a 100755 --- a/docs/content/en/shortcodes/relref.md +++ b/docs/content/en/shortcodes/relref.md @@ -10,7 +10,7 @@ keywords: [] > To override Hugo's embedded `relref` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. > [!note] -> When working with the Markdown [content format], this shortcode has become largely redundant. Its functionality is now primarily handled by [link render hooks], specifically the embedded one provided by Hugo. This hook effectively addresses all the use cases previously covered by this shortcode. +> When working with Markdown, this shortcode is obsolete. Instead, use a [link render hook] that resolves the link destination using the `GetPage` method on the `Page` object. You can either create your own, or simply enable the [embedded link render hook]. The embedded link render hook is automatically enabled for multilingual single-host projects. ## Usage @@ -56,6 +56,7 @@ Rendered: {{% include "_common/ref-and-relref-error-handling.md" %}} [content format]: /content-management/formats/ -[link render hooks]: /render-hooks/links/ +[embedded link render hook]: /render-hooks/links/#default +[link render hook]: /render-hooks/links/ [Markdown notation]: /content-management/shortcodes/#notation [source code]: {{% eturl relref %}} diff --git a/docs/content/en/shortcodes/vimeo.md b/docs/content/en/shortcodes/vimeo.md index c354eefe0..1164ce997 100755 --- a/docs/content/en/shortcodes/vimeo.md +++ b/docs/content/en/shortcodes/vimeo.md @@ -29,19 +29,27 @@ Hugo renders this to: ## Arguments +id +: (string) The video `id`. Optional if the `id` is provided as a positional argument as shown in the example above. + +allowFullScreen +: {{< new-in 0.146.0 />}} +: (`bool`) Whether the `iframe` element can activate full screen mode. Default is `true`. + class : (`string`) The `class` attribute of the wrapping `div` element. Adding one or more CSS classes disables inline styling. -id -: (`string`) The `id` of the Vimeo video +loading +: {{< new-in 0.146.0 />}} +: (`string`) The loading attribute of the `iframe` element, either `eager` or `lazy`. Default is `eager`. title : (`string`) The `title` attribute of the `iframe` element. -If you provide a `class` or `title` you must use a named parameter for the `id`. +Here's an example using some of the available arguments: ```text -{{}} +{{}} ``` ## Privacy diff --git a/docs/content/en/shortcodes/youtube.md b/docs/content/en/shortcodes/youtube.md index 18c5ae6c2..ed3cf0632 100755 --- a/docs/content/en/shortcodes/youtube.md +++ b/docs/content/en/shortcodes/youtube.md @@ -70,7 +70,7 @@ start title : (`string`) The `title` attribute of the `iframe` element. Defaults to "YouTube video". -Example using some of the above: +Here's an example using some of the available arguments: ```text {{}} diff --git a/docs/content/en/templates/embedded.md b/docs/content/en/templates/embedded.md index 198136393..ecfd90514 100644 --- a/docs/content/en/templates/embedded.md +++ b/docs/content/en/templates/embedded.md @@ -1,6 +1,6 @@ --- -title: Embedded templates -description: Hugo provides embedded templates for common use cases. +title: Embedded partial templates +description: Hugo provides embedded partial templates for common use cases. categories: [] keywords: [] weight: 170 @@ -145,6 +145,10 @@ Various optional metadata can also be set: If using YouTube this will produce a og:video tag like ``. Use the `https://youtu.be/` format with YouTube videos (example: `https://youtu.be/qtIqKaDlqXo`). +## Pagination + +See [details](/templates/pagination/). + ## Schema > [!note] diff --git a/docs/content/en/templates/partial.md b/docs/content/en/templates/partial.md index 8493a4674..7ff2d9594 100644 --- a/docs/content/en/templates/partial.md +++ b/docs/content/en/templates/partial.md @@ -49,7 +49,7 @@ As shown in the above example directory structure, you can nest your directories ### Variable scoping -The second argument in a partial call is the variable being passed down. The above examples are passing the `.`, which tells the template receiving the partial to apply the current [context][context]. +The second argument in a partial call is the variable being passed down. The above examples are passing the dot (`.`), which tells the template receiving the partial to apply the current [context][context]. This means the partial will *only* be able to access those variables. The partial is isolated and cannot access the outer scope. From within the partial, `$.Var` is equivalent to `.Var`. diff --git a/docs/content/en/templates/shortcode.md b/docs/content/en/templates/shortcode.md index 711d342cb..3ed573651 100644 --- a/docs/content/en/templates/shortcode.md +++ b/docs/content/en/templates/shortcode.md @@ -329,7 +329,7 @@ You can use the `HasShortcode` method in your base template to conditionally loa [`with`]: /functions/go-template/with/ [content management]: /content-management/shortcodes/ [embedded shortcodes]: /shortcodes/ -[GitHub]: https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates/shortcodes +[GitHub]: https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates/_shortcodes [introduction to templating]: /templates/introduction/ [Markdown notation]: /content-management/shortcodes/#markdown-notation [named or positional]: /content-management/shortcodes/#arguments diff --git a/docs/content/en/troubleshooting/inspection.md b/docs/content/en/troubleshooting/inspection.md index dc662243a..ea3c097f9 100644 --- a/docs/content/en/troubleshooting/inspection.md +++ b/docs/content/en/troubleshooting/inspection.md @@ -34,6 +34,11 @@ Use the [`printf`] function (render) or [`warnf`] function (log to console) to i {{ printf "%[1]v (%[1]T)" $value }} → 42 (int) ``` +{{< new-in 0.146.0 />}} + +Use the [`templates.Current`] function to visually mark template execution boundaries or to display the template call stack. + [`debug.Dump`]: /functions/debug/dump/ [`printf`]: /functions/fmt/printf/ [`warnf`]: /functions/fmt/warnf/ +[`templates.Current`]: /functions/templates/current/ diff --git a/docs/data/embedded_template_urls.toml b/docs/data/embedded_template_urls.toml index f75b14f12..b2a796cd1 100644 --- a/docs/data/embedded_template_urls.toml +++ b/docs/data/embedded_template_urls.toml @@ -4,39 +4,39 @@ # BaseURL 'base_url' = 'https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates' -# Templates -'alias' = 'alias.html' -'disqus' = 'disqus.html' -'google_analytics' = 'google_analytics.html' -'opengraph' = 'opengraph.html' -'pagination' = 'pagination.html' -'robots' = '_default/robots.txt' -'rss' = '_default/rss.xml' -'schema' = 'schema.html' -'sitemap' = '_default/sitemap.xml' -'sitemapindex' = '_default/sitemapindex.xml' -'twitter_cards' = 'twitter_cards.html' +# Partials +'disqus' = '_partials/disqus.html' +'google_analytics' = '_partials/google_analytics.html' +'opengraph' = '_partials/opengraph.html' +'pagination' = '_partials/pagination.html' +'schema' = '_partials/schema.html' +'twitter_cards' = '_partials/twitter_cards.html' # Render hooks -'render-codeblock-goat' = '_default/_markup/render-codeblock-goat.html' -'render-image' = '_default/_markup/render-image.html' -'render-link' = '_default/_markup/render-link.html' -'render-table' = '_default/_markup/render-table.html' +'render-codeblock-goat' = '_markup/render-codeblock-goat.html' +'render-image' = '_markup/render-image.html' +'render-link' = '_markup/render-link.html' +'render-table' = '_markup/render-table.html' # Shortcodes -'details' = 'shortcodes/details.html' -'figure' = 'shortcodes/figure.html' -'gist' = 'shortcodes/gist.html' -'highlight' = 'shortcodes/highlight.html' -'instagram' = 'shortcodes/instagram.html' -'param' = 'shortcodes/param.html' -'qr' = 'shortcodes/qr.html' -'ref' = 'shortcodes/ref.html' -'relref' = 'shortcodes/relref.html' -'twitter' = 'shortcodes/twitter.html' -'twitter_simple' = 'shortcodes/twitter_simple.html' -'vimeo' = 'shortcodes/vimeo.html' -'vimeo_simple' = 'shortcodes/vimeo_simple.html' -'x' = 'shortcodes/x.html' -'x_simple' = 'shortcodes/x_simple.html' -'youtube' = 'shortcodes/youtube.html' +'details' = '_shortcodes/details.html' +'figure' = '_shortcodes/figure.html' +'gist' = '_shortcodes/gist.html' +'highlight' = '_shortcodes/highlight.html' +'instagram' = '_shortcodes/instagram.html' +'param' = '_shortcodes/param.html' +'qr' = '_shortcodes/qr.html' +'ref' = '_shortcodes/ref.html' +'relref' = '_shortcodes/relref.html' +'vimeo' = '_shortcodes/vimeo.html' +'vimeo_simple' = '_shortcodes/vimeo_simple.html' +'x' = '_shortcodes/x.html' +'x_simple' = '_shortcodes/x_simple.html' +'youtube' = '_shortcodes/youtube.html' + +# Other +'alias' = 'alias.html' +'robots' = 'robots.txt' +'rss' = 'rss.xml' +'sitemap' = 'sitemap.xml' +'sitemapindex' = 'sitemapindex.xml' diff --git a/docs/layouts/_default/_markup/render-blockquote.html b/docs/layouts/_markup/render-blockquote.html similarity index 100% rename from docs/layouts/_default/_markup/render-blockquote.html rename to docs/layouts/_markup/render-blockquote.html diff --git a/docs/layouts/_default/_markup/render-codeblock.html b/docs/layouts/_markup/render-codeblock.html similarity index 100% rename from docs/layouts/_default/_markup/render-codeblock.html rename to docs/layouts/_markup/render-codeblock.html diff --git a/docs/layouts/_default/_markup/render-link.html b/docs/layouts/_markup/render-link.html similarity index 98% rename from docs/layouts/_default/_markup/render-link.html rename to docs/layouts/_markup/render-link.html index 88e3cbee5..70011220e 100644 --- a/docs/layouts/_default/_markup/render-link.html +++ b/docs/layouts/_markup/render-link.html @@ -196,7 +196,7 @@ either of these shortcodes in conjunction with this render hook. >{{ .Text }} -{{- define "partials/inline/h-rh-l/validate-fragment.html" }} +{{- define "_partials/inline/h-rh-l/validate-fragment.html" }} {{- /* Validates the fragment portion of a link destination. @@ -248,7 +248,7 @@ either of these shortcodes in conjunction with this render hook. {{- end }} {{- end }} -{{- define "partials/inline/h-rh-l/get-glossary-link-attributes.html" }} +{{- define "_partials/inline/h-rh-l/get-glossary-link-attributes.html" }} {{- /* Returns the anchor element attributes for a link to the given glossary term. diff --git a/docs/layouts/_default/_markup/render-passthrough.html b/docs/layouts/_markup/render-passthrough.html similarity index 100% rename from docs/layouts/_default/_markup/render-passthrough.html rename to docs/layouts/_markup/render-passthrough.html diff --git a/docs/layouts/_default/_markup/render-table.html b/docs/layouts/_markup/render-table.html similarity index 100% rename from docs/layouts/_default/_markup/render-table.html rename to docs/layouts/_markup/render-table.html diff --git a/docs/layouts/partials/docs/functions-aliases.html b/docs/layouts/_partials/docs/functions-aliases.html similarity index 100% rename from docs/layouts/partials/docs/functions-aliases.html rename to docs/layouts/_partials/docs/functions-aliases.html diff --git a/docs/layouts/partials/docs/functions-return-type.html b/docs/layouts/_partials/docs/functions-return-type.html similarity index 100% rename from docs/layouts/partials/docs/functions-return-type.html rename to docs/layouts/_partials/docs/functions-return-type.html diff --git a/docs/layouts/partials/docs/functions-signatures.html b/docs/layouts/_partials/docs/functions-signatures.html similarity index 100% rename from docs/layouts/partials/docs/functions-signatures.html rename to docs/layouts/_partials/docs/functions-signatures.html diff --git a/docs/layouts/partials/helpers/debug/list-item-metadata.html b/docs/layouts/_partials/helpers/debug/list-item-metadata.html similarity index 100% rename from docs/layouts/partials/helpers/debug/list-item-metadata.html rename to docs/layouts/_partials/helpers/debug/list-item-metadata.html diff --git a/docs/layouts/partials/helpers/funcs/color-from-string.html b/docs/layouts/_partials/helpers/funcs/color-from-string.html similarity index 100% rename from docs/layouts/partials/helpers/funcs/color-from-string.html rename to docs/layouts/_partials/helpers/funcs/color-from-string.html diff --git a/docs/layouts/partials/helpers/funcs/get-github-info.html b/docs/layouts/_partials/helpers/funcs/get-github-info.html similarity index 100% rename from docs/layouts/partials/helpers/funcs/get-github-info.html rename to docs/layouts/_partials/helpers/funcs/get-github-info.html diff --git a/docs/layouts/partials/helpers/funcs/get-remote-data.html b/docs/layouts/_partials/helpers/funcs/get-remote-data.html similarity index 100% rename from docs/layouts/partials/helpers/funcs/get-remote-data.html rename to docs/layouts/_partials/helpers/funcs/get-remote-data.html diff --git a/docs/layouts/partials/helpers/gtag.html b/docs/layouts/_partials/helpers/gtag.html similarity index 100% rename from docs/layouts/partials/helpers/gtag.html rename to docs/layouts/_partials/helpers/gtag.html diff --git a/docs/layouts/partials/helpers/linkcss.html b/docs/layouts/_partials/helpers/linkcss.html similarity index 100% rename from docs/layouts/partials/helpers/linkcss.html rename to docs/layouts/_partials/helpers/linkcss.html diff --git a/docs/layouts/partials/helpers/linkjs.html b/docs/layouts/_partials/helpers/linkjs.html similarity index 100% rename from docs/layouts/partials/helpers/linkjs.html rename to docs/layouts/_partials/helpers/linkjs.html diff --git a/docs/layouts/partials/helpers/picture.html b/docs/layouts/_partials/helpers/picture.html similarity index 92% rename from docs/layouts/partials/helpers/picture.html rename to docs/layouts/_partials/helpers/picture.html index 454dd705e..4dc16c002 100644 --- a/docs/layouts/partials/helpers/picture.html +++ b/docs/layouts/_partials/helpers/picture.html @@ -5,6 +5,7 @@ {{ $image1x := $image.Resize (printf "%dx" $width1x) }} {{ $image1xWebp := $image.Resize (printf "%dx webp" $width1x) }} {{ $class := .class | default "h-64 tablet:h-96 lg:h-full w-full object-cover lg:absolute" }} +{{ $loading := .loading | default "eager" }} diff --git a/docs/layouts/partials/helpers/validation/validate-keywords.html b/docs/layouts/_partials/helpers/validation/validate-keywords.html similarity index 100% rename from docs/layouts/partials/helpers/validation/validate-keywords.html rename to docs/layouts/_partials/helpers/validation/validate-keywords.html diff --git a/docs/layouts/partials/layouts/blocks/alert.html b/docs/layouts/_partials/layouts/blocks/alert.html similarity index 100% rename from docs/layouts/partials/layouts/blocks/alert.html rename to docs/layouts/_partials/layouts/blocks/alert.html diff --git a/docs/layouts/partials/layouts/blocks/modal.html b/docs/layouts/_partials/layouts/blocks/modal.html similarity index 100% rename from docs/layouts/partials/layouts/blocks/modal.html rename to docs/layouts/_partials/layouts/blocks/modal.html diff --git a/docs/layouts/partials/layouts/breadcrumbs.html b/docs/layouts/_partials/layouts/breadcrumbs.html similarity index 100% rename from docs/layouts/partials/layouts/breadcrumbs.html rename to docs/layouts/_partials/layouts/breadcrumbs.html diff --git a/docs/layouts/partials/layouts/date.html b/docs/layouts/_partials/layouts/date.html similarity index 100% rename from docs/layouts/partials/layouts/date.html rename to docs/layouts/_partials/layouts/date.html diff --git a/docs/layouts/partials/layouts/docsheader.html b/docs/layouts/_partials/layouts/docsheader.html similarity index 100% rename from docs/layouts/partials/layouts/docsheader.html rename to docs/layouts/_partials/layouts/docsheader.html diff --git a/docs/layouts/partials/layouts/explorer.html b/docs/layouts/_partials/layouts/explorer.html similarity index 100% rename from docs/layouts/partials/layouts/explorer.html rename to docs/layouts/_partials/layouts/explorer.html diff --git a/docs/layouts/partials/layouts/footer.html b/docs/layouts/_partials/layouts/footer.html similarity index 100% rename from docs/layouts/partials/layouts/footer.html rename to docs/layouts/_partials/layouts/footer.html diff --git a/docs/layouts/partials/layouts/head/head-js.html b/docs/layouts/_partials/layouts/head/head-js.html similarity index 100% rename from docs/layouts/partials/layouts/head/head-js.html rename to docs/layouts/_partials/layouts/head/head-js.html diff --git a/docs/layouts/partials/layouts/head/head.html b/docs/layouts/_partials/layouts/head/head.html similarity index 100% rename from docs/layouts/partials/layouts/head/head.html rename to docs/layouts/_partials/layouts/head/head.html diff --git a/docs/layouts/partials/layouts/header/githubstars.html b/docs/layouts/_partials/layouts/header/githubstars.html similarity index 100% rename from docs/layouts/partials/layouts/header/githubstars.html rename to docs/layouts/_partials/layouts/header/githubstars.html diff --git a/docs/layouts/partials/layouts/header/header.html b/docs/layouts/_partials/layouts/header/header.html similarity index 100% rename from docs/layouts/partials/layouts/header/header.html rename to docs/layouts/_partials/layouts/header/header.html diff --git a/docs/layouts/partials/layouts/header/qr.html b/docs/layouts/_partials/layouts/header/qr.html similarity index 96% rename from docs/layouts/partials/layouts/header/qr.html rename to docs/layouts/_partials/layouts/header/qr.html index ccc0ee959..fea64f625 100644 --- a/docs/layouts/partials/layouts/header/qr.html +++ b/docs/layouts/_partials/layouts/header/qr.html @@ -10,7 +10,7 @@ {{ partial "layouts/blocks/modal.html" (dict "modal_button" $qr "modal_content" $qrBig "modal_title" (printf "QR code linking to %s" $.Permalink )) }} -{{ define "partials/_inline/qr" }} +{{ define "_partials/_inline/qr" }} {{ $img_class := .img_class | default "w-10" }} {{ with images.QR $.page.Permalink (dict "targetDir" "images/qr") }} diff --git a/docs/layouts/partials/layouts/header/theme.html b/docs/layouts/_partials/layouts/header/theme.html similarity index 100% rename from docs/layouts/partials/layouts/header/theme.html rename to docs/layouts/_partials/layouts/header/theme.html diff --git a/docs/layouts/partials/layouts/home/features.html b/docs/layouts/_partials/layouts/home/features.html similarity index 100% rename from docs/layouts/partials/layouts/home/features.html rename to docs/layouts/_partials/layouts/home/features.html diff --git a/docs/layouts/partials/layouts/home/opensource.html b/docs/layouts/_partials/layouts/home/opensource.html similarity index 100% rename from docs/layouts/partials/layouts/home/opensource.html rename to docs/layouts/_partials/layouts/home/opensource.html diff --git a/docs/layouts/partials/layouts/home/sponsors.html b/docs/layouts/_partials/layouts/home/sponsors.html similarity index 100% rename from docs/layouts/partials/layouts/home/sponsors.html rename to docs/layouts/_partials/layouts/home/sponsors.html diff --git a/docs/layouts/partials/layouts/hooks/body-end.html b/docs/layouts/_partials/layouts/hooks/body-end.html similarity index 100% rename from docs/layouts/partials/layouts/hooks/body-end.html rename to docs/layouts/_partials/layouts/hooks/body-end.html diff --git a/docs/layouts/partials/layouts/hooks/body-main-start.html b/docs/layouts/_partials/layouts/hooks/body-main-start.html similarity index 100% rename from docs/layouts/partials/layouts/hooks/body-main-start.html rename to docs/layouts/_partials/layouts/hooks/body-main-start.html diff --git a/docs/layouts/partials/layouts/hooks/body-start.html b/docs/layouts/_partials/layouts/hooks/body-start.html similarity index 100% rename from docs/layouts/partials/layouts/hooks/body-start.html rename to docs/layouts/_partials/layouts/hooks/body-start.html diff --git a/docs/layouts/partials/layouts/icons.html b/docs/layouts/_partials/layouts/icons.html similarity index 100% rename from docs/layouts/partials/layouts/icons.html rename to docs/layouts/_partials/layouts/icons.html diff --git a/docs/layouts/partials/layouts/in-this-section.html b/docs/layouts/_partials/layouts/in-this-section.html similarity index 100% rename from docs/layouts/partials/layouts/in-this-section.html rename to docs/layouts/_partials/layouts/in-this-section.html diff --git a/docs/layouts/partials/layouts/page-edit.html b/docs/layouts/_partials/layouts/page-edit.html similarity index 100% rename from docs/layouts/partials/layouts/page-edit.html rename to docs/layouts/_partials/layouts/page-edit.html diff --git a/docs/layouts/partials/layouts/related.html b/docs/layouts/_partials/layouts/related.html similarity index 100% rename from docs/layouts/partials/layouts/related.html rename to docs/layouts/_partials/layouts/related.html diff --git a/docs/layouts/partials/layouts/search/algolialogo.html b/docs/layouts/_partials/layouts/search/algolialogo.html similarity index 100% rename from docs/layouts/partials/layouts/search/algolialogo.html rename to docs/layouts/_partials/layouts/search/algolialogo.html diff --git a/docs/layouts/partials/layouts/search/button.html b/docs/layouts/_partials/layouts/search/button.html similarity index 100% rename from docs/layouts/partials/layouts/search/button.html rename to docs/layouts/_partials/layouts/search/button.html diff --git a/docs/layouts/partials/layouts/search/input.html b/docs/layouts/_partials/layouts/search/input.html similarity index 100% rename from docs/layouts/partials/layouts/search/input.html rename to docs/layouts/_partials/layouts/search/input.html diff --git a/docs/layouts/partials/layouts/search/results.html b/docs/layouts/_partials/layouts/search/results.html similarity index 100% rename from docs/layouts/partials/layouts/search/results.html rename to docs/layouts/_partials/layouts/search/results.html diff --git a/docs/layouts/partials/layouts/templates.html b/docs/layouts/_partials/layouts/templates.html similarity index 100% rename from docs/layouts/partials/layouts/templates.html rename to docs/layouts/_partials/layouts/templates.html diff --git a/docs/layouts/partials/layouts/toc.html b/docs/layouts/_partials/layouts/toc.html similarity index 100% rename from docs/layouts/partials/layouts/toc.html rename to docs/layouts/_partials/layouts/toc.html diff --git a/docs/layouts/partials/opengraph/get-featured-image.html b/docs/layouts/_partials/opengraph/get-featured-image.html similarity index 100% rename from docs/layouts/partials/opengraph/get-featured-image.html rename to docs/layouts/_partials/opengraph/get-featured-image.html diff --git a/docs/layouts/partials/opengraph/opengraph.html b/docs/layouts/_partials/opengraph/opengraph.html similarity index 100% rename from docs/layouts/partials/opengraph/opengraph.html rename to docs/layouts/_partials/opengraph/opengraph.html diff --git a/docs/layouts/shortcodes/chroma-lexers.html b/docs/layouts/_shortcodes/chroma-lexers.html similarity index 100% rename from docs/layouts/shortcodes/chroma-lexers.html rename to docs/layouts/_shortcodes/chroma-lexers.html diff --git a/docs/layouts/shortcodes/code-toggle.html b/docs/layouts/_shortcodes/code-toggle.html similarity index 100% rename from docs/layouts/shortcodes/code-toggle.html rename to docs/layouts/_shortcodes/code-toggle.html diff --git a/docs/layouts/shortcodes/datatable-filtered.html b/docs/layouts/_shortcodes/datatable-filtered.html similarity index 100% rename from docs/layouts/shortcodes/datatable-filtered.html rename to docs/layouts/_shortcodes/datatable-filtered.html diff --git a/docs/layouts/shortcodes/datatable.html b/docs/layouts/_shortcodes/datatable.html similarity index 100% rename from docs/layouts/shortcodes/datatable.html rename to docs/layouts/_shortcodes/datatable.html diff --git a/docs/layouts/shortcodes/deprecated-in.html b/docs/layouts/_shortcodes/deprecated-in.html similarity index 100% rename from docs/layouts/shortcodes/deprecated-in.html rename to docs/layouts/_shortcodes/deprecated-in.html diff --git a/docs/layouts/shortcodes/eturl.html b/docs/layouts/_shortcodes/eturl.html similarity index 100% rename from docs/layouts/shortcodes/eturl.html rename to docs/layouts/_shortcodes/eturl.html diff --git a/docs/layouts/shortcodes/glossary-term.html b/docs/layouts/_shortcodes/glossary-term.html similarity index 100% rename from docs/layouts/shortcodes/glossary-term.html rename to docs/layouts/_shortcodes/glossary-term.html diff --git a/docs/layouts/shortcodes/glossary.html b/docs/layouts/_shortcodes/glossary.html similarity index 100% rename from docs/layouts/shortcodes/glossary.html rename to docs/layouts/_shortcodes/glossary.html diff --git a/docs/layouts/shortcodes/hl.html b/docs/layouts/_shortcodes/hl.html similarity index 100% rename from docs/layouts/shortcodes/hl.html rename to docs/layouts/_shortcodes/hl.html diff --git a/docs/layouts/shortcodes/img.html b/docs/layouts/_shortcodes/img.html similarity index 99% rename from docs/layouts/shortcodes/img.html rename to docs/layouts/_shortcodes/img.html index 60187802e..e49afc57f 100644 --- a/docs/layouts/shortcodes/img.html +++ b/docs/layouts/_shortcodes/img.html @@ -359,7 +359,7 @@ When using the padding filter, provide all arguments in this order: {{- end }} {{- end }} -{{- define "partials/inline/get-resource.html" }} +{{- define "_partials/inline/get-resource.html" }} {{- $r := "" }} {{- $u := urls.Parse .src }} {{- $msg := "The %q shortcode was unable to resolve %s. See %s" }} diff --git a/docs/layouts/shortcodes/imgproc.html b/docs/layouts/_shortcodes/imgproc.html similarity index 100% rename from docs/layouts/shortcodes/imgproc.html rename to docs/layouts/_shortcodes/imgproc.html diff --git a/docs/layouts/shortcodes/include.html b/docs/layouts/_shortcodes/include.html similarity index 100% rename from docs/layouts/shortcodes/include.html rename to docs/layouts/_shortcodes/include.html diff --git a/docs/layouts/shortcodes/list-pages-in-section.html b/docs/layouts/_shortcodes/list-pages-in-section.html similarity index 100% rename from docs/layouts/shortcodes/list-pages-in-section.html rename to docs/layouts/_shortcodes/list-pages-in-section.html diff --git a/docs/layouts/shortcodes/module-mounts-note.html b/docs/layouts/_shortcodes/module-mounts-note.html similarity index 100% rename from docs/layouts/shortcodes/module-mounts-note.html rename to docs/layouts/_shortcodes/module-mounts-note.html diff --git a/docs/layouts/shortcodes/new-in.html b/docs/layouts/_shortcodes/new-in.html similarity index 100% rename from docs/layouts/shortcodes/new-in.html rename to docs/layouts/_shortcodes/new-in.html diff --git a/docs/layouts/shortcodes/per-lang-config-keys.html b/docs/layouts/_shortcodes/per-lang-config-keys.html similarity index 97% rename from docs/layouts/shortcodes/per-lang-config-keys.html rename to docs/layouts/_shortcodes/per-lang-config-keys.html index f6090d555..31d7daf6a 100644 --- a/docs/layouts/shortcodes/per-lang-config-keys.html +++ b/docs/layouts/_shortcodes/per-lang-config-keys.html @@ -14,8 +14,8 @@ separately for each language. (dict "contentDir" "/configuration/all/#contentdir") (dict "copyright" "/configuration/all/#copyright") (dict "disableAliases" "/configuration/all/#disablealiases") - (dict "disableHugoGeneratorInject" "/configuration/all/#disableHugogeneratorinject") - (dict "disableKinds" "/configuration/all/#disableKinds") + (dict "disableHugoGeneratorInject" "/configuration/all/#disablehugogeneratorinject") + (dict "disableKinds" "/configuration/all/#disablekinds") (dict "disableLiveReload" "/configuration/all/#disablelivereload") (dict "disablePathToLower" "/configuration/all/#disablepathtolower") (dict "enableEmoji " "/configuration/all/#enableemoji") diff --git a/docs/layouts/shortcodes/quick-reference.html b/docs/layouts/_shortcodes/quick-reference.html similarity index 100% rename from docs/layouts/shortcodes/quick-reference.html rename to docs/layouts/_shortcodes/quick-reference.html diff --git a/docs/layouts/shortcodes/root-configuration-keys.html b/docs/layouts/_shortcodes/root-configuration-keys.html similarity index 100% rename from docs/layouts/shortcodes/root-configuration-keys.html rename to docs/layouts/_shortcodes/root-configuration-keys.html diff --git a/docs/layouts/shortcodes/syntax-highlighting-styles.html b/docs/layouts/_shortcodes/syntax-highlighting-styles.html similarity index 100% rename from docs/layouts/shortcodes/syntax-highlighting-styles.html rename to docs/layouts/_shortcodes/syntax-highlighting-styles.html diff --git a/docs/layouts/_default/baseof.html b/docs/layouts/baseof.html similarity index 98% rename from docs/layouts/_default/baseof.html rename to docs/layouts/baseof.html index 1f4952146..4c14a6b6d 100644 --- a/docs/layouts/_default/baseof.html +++ b/docs/layouts/baseof.html @@ -44,6 +44,8 @@ {{ block "header" . }} {{ partial "layouts/header/header.html" . }} {{ end }} + {{ block "subheader" . }} + {{ end }} {{ block "hero" . }} {{ end }}
diff --git a/docs/layouts/index.headers b/docs/layouts/home.headers similarity index 100% rename from docs/layouts/index.headers rename to docs/layouts/home.headers diff --git a/docs/layouts/index.html b/docs/layouts/home.html similarity index 100% rename from docs/layouts/index.html rename to docs/layouts/home.html diff --git a/docs/layouts/index.redir b/docs/layouts/home.redir similarity index 100% rename from docs/layouts/index.redir rename to docs/layouts/home.redir diff --git a/docs/layouts/_default/list.html b/docs/layouts/list.html similarity index 100% rename from docs/layouts/_default/list.html rename to docs/layouts/list.html diff --git a/docs/layouts/_default/list.rss.xml b/docs/layouts/list.rss.xml similarity index 100% rename from docs/layouts/_default/list.rss.xml rename to docs/layouts/list.rss.xml diff --git a/docs/layouts/_default/single.html b/docs/layouts/single.html similarity index 100% rename from docs/layouts/_default/single.html rename to docs/layouts/single.html diff --git a/docs/netlify.toml b/docs/netlify.toml index 67c146cad..c24a32a60 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -3,7 +3,7 @@ command = "hugo --gc --minify" [build.environment] - HUGO_VERSION = "0.145.0" + HUGO_VERSION = "0.146.7" [context.production.environment] HUGO_ENV = "production" diff --git a/docs/package.hugo.json b/docs/package.hugo.json new file mode 100644 index 000000000..24ffc7ff5 --- /dev/null +++ b/docs/package.hugo.json @@ -0,0 +1,25 @@ +{ + "name": "hugoDocs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "", + "devDependencies": { + "@awmottaz/prettier-plugin-void-html": "~1.8.0", + "@tailwindcss/cli": "~4.1.0", + "@tailwindcss/typography": "~0.5.16", + "prettier": "~3.5.3", + "prettier-plugin-go-template": "~0.0.15", + "tailwindcss": "~4.1.0" + }, + "dependencies": { + "@alpinejs/focus": "~3.14.9", + "@alpinejs/persist": "~3.14.9", + "@hotwired/turbo": "~8.0.13", + "alpinejs": "~3.14.9" + } +} diff --git a/go.mod b/go.mod index 9423c6f68..fba76335f 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/gohugoio/hugo require ( github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 - github.com/alecthomas/chroma/v2 v2.16.0 + github.com/alecthomas/chroma/v2 v2.17.0 github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c github.com/aws/aws-sdk-go-v2 v1.36.1 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10 @@ -15,7 +15,7 @@ require ( github.com/bep/goportabletext v0.1.0 github.com/bep/gowebp v0.3.0 github.com/bep/helpers v0.5.0 - github.com/bep/imagemeta v0.11.0 + github.com/bep/imagemeta v0.12.0 github.com/bep/lazycache v0.8.0 github.com/bep/logg v0.4.0 github.com/bep/mclib v1.20400.20402 @@ -26,7 +26,7 @@ require ( github.com/clbanning/mxj/v2 v2.7.0 github.com/disintegration/gift v1.2.1 github.com/dustin/go-humanize v1.0.1 - github.com/evanw/esbuild v0.25.2 + github.com/evanw/esbuild v0.25.3 github.com/fatih/color v1.18.0 github.com/fortytw2/leaktest v1.3.0 github.com/frankban/quicktest v1.14.6 @@ -69,8 +69,8 @@ require ( github.com/tdewolff/minify/v2 v2.20.37 github.com/tdewolff/parse/v2 v2.7.15 github.com/tetratelabs/wazero v1.9.0 - github.com/yuin/goldmark v1.7.8 - github.com/yuin/goldmark-emoji v1.0.5 + github.com/yuin/goldmark v1.7.10 + github.com/yuin/goldmark-emoji v1.0.6 go.uber.org/automaxprocs v1.5.3 gocloud.dev v0.40.0 golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 diff --git a/go.sum b/go.sum index 5712679f0..b432e1d6b 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= -github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/chroma/v2 v2.17.0 h1:3r2Cgk+nXNICMBxIFGnTRTbQFUwMiLisW+9uos0TtUI= +github.com/alecthomas/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= @@ -139,8 +139,8 @@ github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= github.com/bep/helpers v0.5.0 h1:rneezhnG7GzLFlsEWO/EnleaBRuluBDGFimalO6Y50o= github.com/bep/helpers v0.5.0/go.mod h1:dSqCzIvHbzsk5YOesp1M7sKAq5xUcvANsRoKdawxH4Q= -github.com/bep/imagemeta v0.11.0 h1:jL92HhL1H70NC+f8OVVn5D/nC3FmdxTnM3R+csj54mE= -github.com/bep/imagemeta v0.11.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= +github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k= +github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8= github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= @@ -186,8 +186,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanw/esbuild v0.25.2 h1:ublSEmZSjzOc6jLO1OTQy/vHc1wiqyDF4oB3hz5sM6s= -github.com/evanw/esbuild v0.25.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U= +github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -466,11 +466,10 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= -github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= +github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/hugolib/alias.go b/hugolib/alias.go index 3beee44db..7b252c613 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -53,9 +53,10 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err if ps, ok := p.(*pageState); ok { base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor() } - templateDesc.Layout = "" + templateDesc.LayoutFromUser = "" templateDesc.Kind = "" templateDesc.OutputFormat = output.AliasHTMLFormat.Name + templateDesc.MediaType = output.AliasHTMLFormat.MediaType.Type q := tplimpl.TemplateQuery{ Path: base, diff --git a/hugolib/collections_test.go b/hugolib/collections_test.go index a8c817bec..f62d4c604 100644 --- a/hugolib/collections_test.go +++ b/hugolib/collections_test.go @@ -39,6 +39,8 @@ title: "Page" `) b.CreateSites().Build(BuildCfg{}) + // b.H.TemplateStore.PrintDebug("", tplimpl.CategoryLayout, os.Stdout) + c.Assert(len(b.H.Sites), qt.Equals, 1) c.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 2) diff --git a/hugolib/config_test.go b/hugolib/config_test.go index cbf821ee7..4275fb02a 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -475,7 +475,7 @@ name = "menu-theme" }) }) - // Issue #8724 + // Issue #8724 ##13643 for _, mergeStrategy := range []string{"none", "shallow"} { c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) { smapConfigTempl := `[sitemap] @@ -495,7 +495,7 @@ name = "menu-theme" b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "", Disable: false, Priority: -1, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "schemas/sitemap") } else { - b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: -1, Filename: "sitemap.xml"}) + b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: 0.5, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "monthly") } }) diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index b501cd9ea..c51b4a17a 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -180,7 +180,7 @@ func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Iden if p.Component() == files.ComponentFolderContent { // It may also be a bundled content resource. - key := p.ForBundleType(paths.TypeContentResource).Base() + key := p.ForType(paths.TypeContentResource).Base() tree = t.treeResources nCount = 0 tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(), diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go index aed2a7f13..f72862150 100644 --- a/hugolib/content_map_test.go +++ b/hugolib/content_map_test.go @@ -517,7 +517,7 @@ func TestHTMLNotContent(t *testing.T) { -- hugo.toml.temp -- [contentTypes] [contentTypes."text/markdown"] -# Emopty for now. +# Empty for now. -- hugo.yaml.temp -- contentTypes: text/markdown: {} @@ -538,6 +538,7 @@ title: p1 -- content/p1/c.html --

c

-- layouts/_default/single.html -- +Path: {{ .Path }}|{{.Kind }} |{{ (.Resources.Get "a.html").RelPermalink -}} |{{ (.Resources.Get "b.html").RelPermalink -}} |{{ (.Resources.Get "c.html").Publish }} diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go index 56ae0a052..c4e15a5c6 100644 --- a/hugolib/content_render_hooks_test.go +++ b/hugolib/content_render_hooks_test.go @@ -464,8 +464,6 @@ title: "Home" ` b := Test(t, files) - // b.DebugPrint("", tplimpl.CategoryShortcode) - b.AssertFileContentExact("public/index.xml", "My shortcode XML.") b.AssertFileContentExact("public/index.html", "My shortcode HTML.") s := b.H.Sites[0] diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index 3e9b92087..b32b8796f 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -397,7 +397,7 @@ func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo { }) // Filter out any mounts not belonging to this filesystem. - // TODO(bep) I think this is superflous. + // TODO(bep) I think this is superfluous. n := 0 for _, mm := range m { if mm.Meta().Component == d.Name { diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 4ea6f420d..3c2f1ad74 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -918,7 +918,7 @@ type IntegrationTestConfig struct { // The files to use on txtar format, see // https://pkg.go.dev/golang.org/x/exp/cmd/txtar - // There are some conentions used in this test setup. + // There are some contentions used in this test setup. // - §§§ can be used to wrap code fences. // - §§ can be used to wrap multiline strings. // - filenames prefixed with sourcefilename: will be read from the file system relative to the current dir. diff --git a/hugolib/page.go b/hugolib/page.go index b36b9712c..84d4d1ea8 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -482,12 +482,12 @@ func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimp f := po.f base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type) return base, tplimpl.TemplateDescriptor{ - Kind: p.Kind(), - Lang: p.Language().Lang, - Layout: p.Layout(), - OutputFormat: f.Name, - MediaType: f.MediaType.Type, - IsPlainText: f.IsPlainText, + Kind: p.Kind(), + Lang: p.Language().Lang, + LayoutFromUser: p.Layout(), + OutputFormat: f.Name, + MediaType: f.MediaType.Type, + IsPlainText: f.IsPlainText, } } @@ -495,8 +495,8 @@ func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool dir, d := p.GetInternalTemplateBasePathAndDescriptor() if len(layouts) > 0 { - d.Layout = layouts[0] - d.LayoutMustMatch = true + d.LayoutFromUser = layouts[0] + d.LayoutFromUserMustMatch = true } q := tplimpl.TemplateQuery{ diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 5f7d6f930..20abb7884 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -850,7 +850,7 @@ func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlai }) if err != nil { if herrors.IsTimeoutError(err) { - err = fmt.Errorf("timed out rendering the page content. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file: %w", err) + err = fmt.Errorf("timed out rendering the page content. Extend the `timeout` limit in your Hugo config file: %w", err) } return contentPlainPlainWords{}, err } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 3ac0940e2..cc8a145d9 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -677,13 +677,7 @@ Loop: // Used to check if the template expects inner content, // so just pick one arbitrarily with the same name. - q := tplimpl.TemplateQuery{ - Path: "", - Name: sc.name, - Category: tplimpl.CategoryShortcode, - Consider: nil, - } - templ := s.s.TemplateStore.LookupShortcode(q) + templ := s.s.TemplateStore.LookupShortcodeByName(sc.name) if templ == nil { return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name) } diff --git a/hugolib/site_test.go b/hugolib/site_test.go index a9fe977cf..4d68602e5 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -978,7 +978,7 @@ func TestRefLinking(t *testing.T) { {".", "", true, "/level2/level3/"}, {"./", "", true, "/level2/level3/"}, - {"embedded.dot.md", "", true, "/level2/level3/embedded/"}, + {"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"}, // test empty link, as well as fragment only link {"", "", true, ""}, diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 1c2642468..922ecbc12 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -139,7 +139,7 @@ weight = 1 languageName = "English" [languages.nn] weight = 2 --- layouts/_default/list.xml -- +-- layouts/list.xml -- Site: {{ .Site.Title }}| -- layouts/home -- Home. diff --git a/hugoreleaser.env b/hugoreleaser.env index c88ba3e46..9d9a10112 100644 --- a/hugoreleaser.env +++ b/hugoreleaser.env @@ -1,7 +1,15 @@ # Release env. # These will be replaced by script before release. -HUGORELEASER_TAG=v0.146.0 -HUGORELEASER_COMMITISH=5d1b9d39858bb0b2e505af9f649bfb55295ecca1 +HUGORELEASER_TAG=v0.147.0 +HUGORELEASER_COMMITISH=7d0039b86ddd6397816cc3383cb0cfa481b15f32 + + + + + + + + diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go index 1655ea513..419fbf4d2 100644 --- a/parser/metadecoders/decoder.go +++ b/parser/metadecoders/decoder.go @@ -36,16 +36,22 @@ import ( // Decoder provides some configuration options for the decoders. type Decoder struct { - // Delimiter is the field delimiter used in the CSV decoder. It defaults to ','. + // Delimiter is the field delimiter. Used in the CSV decoder. Default is + // ','. Delimiter rune - // Comment, if not 0, is the comment character used in the CSV decoder. Lines beginning with the - // Comment character without preceding whitespace are ignored. + // Comment, if not 0, is the comment character. Lines beginning with the + // Comment character without preceding whitespace are ignored. Used in the + // CSV decoder. Comment rune // If true, a quote may appear in an unquoted field and a non-doubled quote - // may appear in a quoted field. It defaults to false. + // may appear in a quoted field. Used in the CSV decoder. Default is false. LazyQuotes bool + + // The target data type, either slice or map. Used in the CSV decoder. + // Default is slice. + TargetType string } // OptionsKey is used in cache keys. @@ -54,12 +60,14 @@ func (d Decoder) OptionsKey() string { sb.WriteRune(d.Delimiter) sb.WriteRune(d.Comment) sb.WriteString(strconv.FormatBool(d.LazyQuotes)) + sb.WriteString(d.TargetType) return sb.String() } // Default is a Decoder in its default configuration. var Default = Decoder{ - Delimiter: ',', + Delimiter: ',', + TargetType: "slice", } // UnmarshalToMap will unmarshall data in format f into a new map. This is @@ -122,7 +130,14 @@ func (d Decoder) Unmarshal(data []byte, f Format) (any, error) { if len(data) == 0 { switch f { case CSV: - return make([][]string, 0), nil + switch d.TargetType { + case "map": + return make(map[string]any), nil + case "slice": + return make([][]string, 0), nil + default: + return nil, fmt.Errorf("invalid targetType: expected either slice or map, received %s", d.TargetType) + } default: return make(map[string]any), nil } @@ -232,10 +247,36 @@ func (d Decoder) unmarshalCSV(data []byte, v any) error { switch vv := v.(type) { case *any: - *vv = records - default: - return fmt.Errorf("CSV cannot be unmarshaled into %T", v) + switch d.TargetType { + case "map": + if len(records) < 2 { + return fmt.Errorf("cannot unmarshal CSV into %T: expected at least a header row and one data row", v) + } + seen := make(map[string]bool, len(records[0])) + for _, fieldName := range records[0] { + if seen[fieldName] { + return fmt.Errorf("cannot unmarshal CSV into %T: header row contains duplicate field names", v) + } + seen[fieldName] = true + } + + sm := make([]map[string]string, len(records)-1) + for i, record := range records[1:] { + m := make(map[string]string, len(records[0])) + for j, col := range record { + m[records[0][j]] = col + } + sm[i] = m + } + *vv = sm + case "slice": + *vv = records + default: + return fmt.Errorf("cannot unmarshal CSV into %T: invalid targetType: expected either slice or map, received %s", v, d.TargetType) + } + default: + return fmt.Errorf("cannot unmarshal CSV into %T", v) } return nil diff --git a/resources/images/filters.go b/resources/images/filters.go index 9c2b9b46f..1e44f1184 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -79,6 +79,7 @@ func (*Filters) Text(text string, options ...any) gift.Filter { x: 10, y: 10, alignx: "left", + aligny: "top", linespacing: 2, } @@ -102,6 +103,11 @@ func (*Filters) Text(text string, options ...any) gift.Filter { if tf.alignx != "left" && tf.alignx != "center" && tf.alignx != "right" { panic("alignx must be one of left, center, right") } + case "aligny": + tf.aligny = cast.ToString(v) + if tf.aligny != "top" && tf.aligny != "center" && tf.aligny != "bottom" { + panic("aligny must be one of top, center, bottom") + } case "linespacing": tf.linespacing = cast.ToInt(v) diff --git a/resources/images/images_golden_integration_test.go b/resources/images/images_golden_integration_test.go index c49de7bd1..5397bee23 100644 --- a/resources/images/images_golden_integration_test.go +++ b/resources/images/images_golden_integration_test.go @@ -252,8 +252,8 @@ Home. "linespacing" 8 "size" 28 "x" (div $sunset.Width 2 | int) + "y" (div $sunset.Height 2 | int) "alignx" "center" - "y" 190 }} {{ $text := "Pariatur deserunt sunt nisi sunt tempor quis eu. Sint et nulla enim officia sunt cupidatat. Eu amet ipsum qui velit cillum cillum ad Lorem in non ad aute." }} @@ -262,6 +262,11 @@ Home. {{ template "filters" (dict "name" "text_alignx-right.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }} {{ $textOpts = (dict "alignx" "left") | merge $textOpts }} {{ template "filters" (dict "name" "text_alignx-left.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }} +{{ $textOpts = (dict "alignx" "center" "aligny" "center") | merge $textOpts }} +{{ $text = "Est exercitation deserunt exercitation nostrud magna. Eiusmod anim deserunt sit elit dolore ea incididunt nisi. Ea ullamco excepteur voluptate occaecat duis pariatur proident cupidatat. Eu id esse qui consectetur commodo ad ex esse cupidatat velit duis cupidatat. Aliquip irure tempor consequat non amet in mollit ipsum officia tempor laborum." }} +{{ template "filters" (dict "name" "text_alignx-center_aligny-center.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }} +{{ $textOpts = (dict "alignx" "center" "aligny" "bottom") | merge $textOpts }} +{{ template "filters" (dict "name" "text_alignx-center_aligny-bottom.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }} {{ define "filters"}} {{ if lt (len (path.Ext .name)) 4 }} @@ -279,6 +284,8 @@ Home. opts.T = t opts.Name = name opts.Files = files + // opts.WriteFiles = true + // opts.DevMode = true imagetesting.RunGolden(opts) } diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg index 090600f5f..94bcb811a 100644 Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg differ diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg new file mode 100644 index 000000000..ca8893e4e Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg differ diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg new file mode 100644 index 000000000..828b6e6a3 Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg differ diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg index d77e301df..2894fae42 100644 Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg differ diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg index 3b727234a..207e88a49 100644 Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg differ diff --git a/resources/images/text.go b/resources/images/text.go index 324878839..f3943a475 100644 --- a/resources/images/text.go +++ b/resources/images/text.go @@ -36,6 +36,7 @@ type textFilter struct { color color.Color x, y int alignx string + aligny string size float64 linespacing int fontSource hugio.ReadSeekCloserProvider @@ -110,12 +111,19 @@ func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) } finalLines = append(finalLines, currentLine) } + // Total height of the text from the top of the first line to the baseline of the last line + totalHeight := len(finalLines)*fontHeight + (len(finalLines)-1)*f.linespacing // Correct y position based on font and size - f.y = f.y + fontHeight - - // Start position - y := f.y + y := f.y + fontHeight + switch f.aligny { + case "top": + // Do nothing + case "center": + y = y - totalHeight/2 + case "bottom": + y = y - totalHeight + } // Draw text line by line for _, line := range finalLines { diff --git a/testscripts/commands/new.txt b/testscripts/commands/new.txt index 433e238bf..cd338203f 100644 --- a/testscripts/commands/new.txt +++ b/testscripts/commands/new.txt @@ -36,8 +36,8 @@ checkfile content/posts/post-3/bryce-canyon.jpg checkfile content/posts/post-3/index.md checkfile layouts/baseof.html checkfile layouts/home.html -checkfile layouts/list.html -checkfile layouts/single.html +checkfile layouts/section.html +checkfile layouts/page.html checkfile layouts/taxonomy.html checkfile layouts/term.html checkfile layouts/_partials/footer.html diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go index cc60770f9..b60aaea87 100644 --- a/tpl/collections/collections_integration_test.go +++ b/tpl/collections/collections_integration_test.go @@ -278,3 +278,23 @@ disableKinds = ['rss','sitemap', 'taxonomy', 'term', 'page'] b.AssertFileContentExact("public/index.html", "0: /a3_b1.html\n\n1: /b2.html\n\n2: /a1.html\n\n3: /a2.html\n$") } + +// Issue 13621. +func TestWhereNotInEmptySlice(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +{{- $pages := where site.RegularPages "Kind" "not in" (slice) -}} +Len: {{ $pages | len }}| +-- layouts/all.html -- +All|{{ .Title }}| +-- content/p1.md -- + +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/index.html", "Len: 1|") +} diff --git a/tpl/collections/where.go b/tpl/collections/where.go index b15cfe781..ee49d0bbb 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -138,6 +138,9 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error } if mv.Len() == 0 { + if op == "not in" { + return true, nil + } return false, nil } diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go index 60f97e607..ecf748f93 100644 --- a/tpl/collections/where_test.go +++ b/tpl/collections/where_test.go @@ -761,6 +761,7 @@ func TestCheckCondition(t *testing.T) { expect{true, false}, }, {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{}), "not in", expect{true, false}}, {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, { reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index b9ef4b244..19882e36a 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -24,13 +24,13 @@ import ( "time" "github.com/bep/lazycache" - "github.com/gohugoio/hugo/common/constants" "github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/identity" + texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" "github.com/gohugoio/hugo/tpl" - texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" + "github.com/gohugoio/hugo/tpl/tplimpl" bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/deps" @@ -109,7 +109,7 @@ func (c *contextWrapper) Set(in any) string { // A string if the partial is a text/template, or template.HTML when html/template. // Note that ctx is provided by Hugo, not the end user. func (ns *Namespace) Include(ctx context.Context, name string, contextList ...any) (any, error) { - res := ns.includWithTimeout(ctx, name, contextList...) + res := ns.include(ctx, name, contextList...) if res.err != nil { return nil, res.err } @@ -121,49 +121,36 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an return res.result, nil } -func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult { +func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult { + v, err := ns.lookup(name) + if err != nil { + return includeResult{err: err} + } + return ns.doInclude(ctx, v, dataList...) +} + +func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) { if strings.HasPrefix(name, "partials/") { // This is most likely not what the user intended. // This worked before Hugo 0.146.0. ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name) } - // Create a new context with a timeout not connected to the incoming context. - timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout()) - defer cancel() - - res := make(chan includeResult, 1) - - go func() { - res <- ns.include(ctx, name, dataList...) - }() - - select { - case r := <-res: - return r - case <-timeoutCtx.Done(): - err := timeoutCtx.Err() - if err == context.DeadlineExceeded { - //lint:ignore ST1005 end user message. - err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Conf.Timeout()) - } - return includeResult{err: err} + v := ns.deps.TemplateStore.LookupPartial(name) + if v == nil { + return nil, fmt.Errorf("partial %q not found", name) } + return v, nil } // include is a helper function that lookups and executes the named partial. // Returns the final template name and the rendered output. -func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult { +func (ns *Namespace) doInclude(ctx context.Context, templ *tplimpl.TemplInfo, dataList ...any) includeResult { var data any if len(dataList) > 0 { data = dataList[0] } - v := ns.deps.TemplateStore.LookupPartial(name) - if v == nil { - return includeResult{err: fmt.Errorf("partial %q not found", name)} - } - templ := v - info := v.ParseInfo + info := templ.ParseInfo var w io.Writer @@ -212,6 +199,20 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any Variants: variants, } depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx) + ti, err := ns.lookup(name) + if err != nil { + return nil, err + } + + if parent := tpl.Context.CurrentTemplate.Get(ctx); parent != nil { + for parent != nil { + if parent.CurrentTemplateInfoOps == ti { + // This will deadlock if we continue. + return nil, fmt.Errorf("circular call stack detected in partial %q", ti.Filename()) + } + parent = parent.Parent + } + } r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) { var depsManagerShared identity.Manager @@ -221,7 +222,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any depsManagerShared = identity.NewManager("partials") ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider)) } - r := ns.includWithTimeout(ctx, key.Name, context) + r := ns.doInclude(ctx, ti, context) if ns.deps.Conf.Watching() { r.mangager = depsManagerShared } diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go index 6fab3abd8..0fa47104d 100644 --- a/tpl/partials/partials_integration_test.go +++ b/tpl/partials/partials_integration_test.go @@ -256,7 +256,6 @@ func TestIncludeTimeout(t *testing.T) { files := ` -- config.toml -- baseURL = 'http://example.com/' -timeout = '200ms' -- layouts/index.html -- {{ partials.Include "foo.html" . }} -- layouts/partials/foo.html -- @@ -271,7 +270,7 @@ timeout = '200ms' ).BuildE() b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, "timed out") + b.Assert(err.Error(), qt.Contains, "maximum template call stack size exceeded") } func TestIncludeCachedTimeout(t *testing.T) { @@ -284,6 +283,8 @@ timeout = '200ms' -- layouts/index.html -- {{ partials.IncludeCached "foo.html" . }} -- layouts/partials/foo.html -- +{{ partialCached "bar.html" . }} +-- layouts/partials/bar.html -- {{ partialCached "foo.html" . }} ` @@ -295,7 +296,7 @@ timeout = '200ms' ).BuildE() b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, "timed out") + b.Assert(err.Error(), qt.Contains, `error calling partialCached: circular call stack detected in partial`) } // See Issue #10789 diff --git a/tpl/template.go b/tpl/template.go index f69ae2210..877422123 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -160,6 +160,7 @@ type CurrentTemplateInfoCommonOps interface { // CurrentTemplateInfo as returned in templates.Current. type CurrentTemplateInfo struct { Parent *CurrentTemplateInfo + Level int CurrentTemplateInfoOps } diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go index a0dcf0348..d16333ed4 100644 --- a/tpl/templates/templates_integration_test.go +++ b/tpl/templates/templates_integration_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2025 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. @@ -17,6 +17,7 @@ import ( "path/filepath" "testing" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugolib" ) @@ -166,3 +167,135 @@ p3.current.Ancestors.Reverse: {{ with templates.Current }}{{ range .Ancestors.Re "p2.current.Ancestors: _partials/p1.html|all.html", ) } + +func TestBaseOfIssue13583(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/_index.md -- +--- +title: "Home" +outputs: ["html", "amp"] +--- +title: "Home" +-- layouts/baseof.html -- +layouts/baseof.html +{{ block "main" . }}{{ end }} +-- layouts/baseof.amp.html -- +layouts/baseof.amp.html +{{ block "main" . }}{{ end }} +-- layouts/home.html -- +{{ define "main" }} +Home. +{{ end }} + +` + b := hugolib.Test(t, files) + b.AssertFileContent("public/index.html", "layouts/baseof.html") + b.AssertFileContent("public/amp/index.html", "layouts/baseof.amp.html") +} + +func TestAllVsAmp(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/_index.md -- +--- +title: "Home" +outputs: ["html", "amp"] +--- +title: "Home" +-- layouts/all.html -- +All. + +` + b := hugolib.Test(t, files) + b.AssertFileContent("public/index.html", "All.") + b.AssertFileContent("public/amp/index.html", "All.") +} + +// Issue #13584. +func TestLegacySectionSection(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/mysection/_index.md -- +-- layouts/section/section.html -- +layouts/section/section.html + +` + b := hugolib.Test(t, files) + b.AssertFileContent("public/mysection/index.html", "layouts/section/section.html") +} + +func TestErrorMessageParseError(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +Line 1. +Line 2. {{ foo }} <- this func does not exist. +Line 3. +` + + b, err := hugolib.TestE(t, files) + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"/layouts/home.html:2:1": parse of template failed: template: home.html:2: function "foo" not defined`)) +} + +func TestErrorMessageExecuteError(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +Line 1. +Line 2. {{ .Foo }} <- this method does not exist. +Line 3. +` + + b, err := hugolib.TestE(t, files) + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, filepath.FromSlash(` "/layouts/home.html:2:11": execute of template failed`)) +} + +func TestPartialReturnPanicIssue13600(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +Partial: {{ partial "p1.html" . }} +-- layouts/_partials/p1.html -- +P1. +{{ return ( delimit . ", " ) | string }} +` + + b, err := hugolib.TestE(t, files) + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, "wrong number of args for string: want 1 got 0") +} + +func TestPartialWithoutSuffixIssue13601(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +P1: {{ partial "p1" . }} +P2: {{ partial "p2" . }} +-- layouts/_partials/p1 -- +P1. +-- layouts/_partials/p2 -- +P2. +{{ return "foo bar" }} + +` + + b := hugolib.Test(t, files) + b.AssertFileContent("public/index.html", "P1: P1.\nP2: foo bar") +} diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html index 17188c6fd..804038e7d 100644 --- a/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html +++ b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html @@ -3,7 +3,7 @@ {{- with .Get 0 -}} {{- template "render-instagram" (dict "id" . "pc" $pc) -}} {{- else -}} - {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}} + {{- errorf "The %q shortcode requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}} {{- end -}} {{- end -}} diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html index 3ce470c6e..2588ac86c 100644 --- a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html +++ b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html @@ -4,11 +4,11 @@ Renders an embedded Vimeo video. Accepts named or positional arguments. If positional, order is id, class, title, then loading. -@param {string} [id] The video id. Optional if the id is provided as first positional argument. +@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode. @param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element. +@param {string} [id] The video id. Optional if the id is the first and only positional argument. @param {string} [loading=eager] The loading attribute of the iframe element. @param {string} [title=Vimeo video] The title attribute of the iframe element. -@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode. @returns {template.HTML} @@ -22,16 +22,16 @@ title, then loading. {{- else }} {{- $dnt := cond $pc.EnableDNT 1 0 }} - {{- $id := or (.Get "id") (.Get 0) "" }} - {{- $class := or (.Get "class") (.Get 1) "" }} - {{- $title := or (.Get "title") (.Get 2) "Vimeo video" }} - {{- $loading := or (.Get "loading") (.Get 3) "eager" }} - {{- $allowFullScreen := or (.Get "allowFullScreen") (.Get 4) true }} + {{- $allowFullScreen := true }} + {{- $class := or (.Get "class") }} + {{- $id := or (.Get "id") (.Get 0) }} + {{- $loading := or (.Get "loading") }} + {{- $title := or (.Get "title") }} - {{- if in (slice "false" false 0) ($.Get "allowFullScreen") }} - {{- $allowFullScreen = false }} - {{- else if in (slice "true" true 1) ($.Get "allowFullScreen") }} + {{- if in (slice "true" true 1) (.Get "allowFullScreen") }} {{- $allowFullScreen = true }} + {{- else if in (slice "false" false 0) (.Get "allowFullScreen") }} + {{- $allowFullScreen = false }} {{- end }} {{- $iframeAllowList := "" }} diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html index 11f19b1f6..86a6dfc9f 100644 --- a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html +++ b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html @@ -6,14 +6,14 @@ {{- $ctx = merge $ctx (dict "id" . "class" ($.Get "class")) -}} {{- template "render-vimeo" $ctx -}} {{- else -}} - {{- errorf "The %q shortocde requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}} + {{- errorf "The %q shortcode requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}} {{- end -}} {{- else -}} {{- with .Get 0 -}} {{- $ctx = merge $ctx (dict "id" . "class" ($.Get 1)) -}} {{- template "render-vimeo" $ctx -}} {{- else -}} - {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}} + {{- errorf "The %q shortcode requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}} {{- end -}} {{- end -}} {{- end -}} diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html index cebe50626..18b086944 100644 --- a/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html +++ b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html @@ -6,7 +6,7 @@ Renders an embedded YouTube video. @param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element. @param {bool} [controls=true] Whether to display the video controls. @param {int} [end] The time, measured in seconds from the start of the video, when the player should stop playing the video. -@param {string} [id] The video id. Optional if the id is provided as first positional argument. +@param {string} [id] The video id. Optional if the id is the first and only positional argument. @param {string} [loading=eager] The loading attribute of the iframe element. @param {bool} [loop=false] Whether to indefinitely repeat the video. Ignores the start and end arguments after the first play. @param {bool} [mute=false] Whether to mute the video. Always true when autoplay is true. @@ -41,27 +41,29 @@ Renders an embedded YouTube video. {{- /* Get arguments. */}} {{- if in (slice "true" true 1) ($.Get "allowFullScreen") }} - {{- $iframeAllowList = printf "%s; fullscreen" $iframeAllowList }} + {{- $allowFullScreen = true }} + {{- else if in (slice "false" false 0) ($.Get "allowFullScreen") }} + {{- $allowFullScreen = false }} {{- end }} - {{- if in (slice "false" false 0) ($.Get "autoplay") }} - {{- $autoplay = 0 }} - {{- else if in (slice "true" true 1) ($.Get "autoplay") }} + {{- if in (slice "true" true 1) ($.Get "autoplay") }} {{- $autoplay = 1 }} + {{- else if in (slice "false" false 0) ($.Get "autoplay") }} + {{- $autoplay = 0 }} {{- end }} - {{- if in (slice "false" false 0) ($.Get "controls") }} - {{- $controls = 0 }} - {{- else if in (slice "true" true 1) ($.Get "controls") }} + {{- if in (slice "true" true 1) ($.Get "controls") }} {{- $controls = 1 }} + {{- else if in (slice "false" false 0) ($.Get "controls") }} + {{- $controls = 0 }} {{- end }} - {{- if in (slice "false" false 0) ($.Get "loop") }} - {{- $loop = 0 }} - {{- else if in (slice "true" true 1) ($.Get "loop") }} + {{- if in (slice "true" true 1) ($.Get "loop") }} {{- $loop = 1 }} + {{- else if in (slice "false" false 0) ($.Get "loop") }} + {{- $loop = 0 }} {{- end }} - {{- if in (slice "false" false 0) ($.Get "mute") }} - {{- $mute = 0 }} - {{- else if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }} + {{- if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }} {{- $mute = 1 }} + {{- else if in (slice "false" false 0) ($.Get "mute") }} + {{- $mute = 0 }} {{- end }} {{- $class := or ($.Get "class") $class }} {{- $end := or ($.Get "end") $end }} @@ -69,6 +71,11 @@ Renders an embedded YouTube video. {{- $start := or ($.Get "start") $start }} {{- $title := or ($.Get "title") $title }} + {{- /* Adjust iframeAllowList. */}} + {{- if $allowFullScreen }} + {{- $iframeAllowList = printf "%s; fullscreen" $iframeAllowList }} + {{- end }} + {{- /* Define src attribute. */}} {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }} {{- $src := printf "https://%s/embed/%s" $host $id }} diff --git a/tpl/tplimpl/legacy_integration_test.go b/tpl/tplimpl/legacy_integration_test.go new file mode 100644 index 000000000..a96e35fca --- /dev/null +++ b/tpl/tplimpl/legacy_integration_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// 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 tplimpl_test + +import ( + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestLegacyPartialIssue13599(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/partials/mypartial.html -- +Mypartial. +-- layouts/_default/index.html -- +mypartial: {{ template "partials/mypartial.html" . }} + +` + b := hugolib.Test(t, files) + + b.AssertFileContent("public/index.html", "Mypartial.") +} diff --git a/tpl/tplimpl/shortcodes_integration_test.go b/tpl/tplimpl/shortcodes_integration_test.go index 9c541a1e2..9d7af4a3d 100644 --- a/tpl/tplimpl/shortcodes_integration_test.go +++ b/tpl/tplimpl/shortcodes_integration_test.go @@ -488,9 +488,9 @@ Content: {{ .Content }} // Regular mode b := hugolib.Test(t, files) - b.AssertFileContent("public/p1/index.html", "f7687b0c4e85b7d4") - b.AssertFileContent("public/p2/index.html", "f7687b0c4e85b7d4") - b.AssertFileContent("public/p3/index.html", "caca499bdc7f1e1e") + b.AssertFileContent("public/p1/index.html", "82566e6b8d04b53e") + b.AssertFileContent("public/p2/index.html", "82566e6b8d04b53e") + b.AssertFileContent("public/p3/index.html", "2b5f9cc3167d1336") // Simple mode files = strings.ReplaceAll(files, "privacy.vimeo.simple = false", "privacy.vimeo.simple = true") @@ -687,12 +687,12 @@ title: p2 b := hugolib.Test(t, files) - b.AssertFileContent("public/p1/index.html", "5156322adda11844") + b.AssertFileContent("public/p1/index.html", "4b54bf9bd03946ec") b.AssertFileContent("public/p2/index.html", "289c655e727e596c") files = strings.ReplaceAll(files, "privacy.youtube.privacyEnhanced = false", "privacy.youtube.privacyEnhanced = true") b = hugolib.Test(t, files) - b.AssertFileContent("public/p1/index.html", "599174706edf963a") + b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f") b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1") } diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go index 21649b032..ea47afc88 100644 --- a/tpl/tplimpl/templatedescriptor.go +++ b/tpl/tplimpl/templatedescriptor.go @@ -22,8 +22,9 @@ const baseNameBaseof = "baseof" // This is used both as a key and in lookups. type TemplateDescriptor struct { // Group 1. - Kind string // page, home, section, taxonomy, term (and only those) - Layout string // list, single, baseof, mycustomlayout. + Kind string // page, home, section, taxonomy, term (and only those) + LayoutFromTemplate string // list, single, all,mycustomlayout + LayoutFromUser string // custom layout set in front matter, e.g. list, single, all, mycustomlayout // Group 2. OutputFormat string // rss, csv ... @@ -34,23 +35,21 @@ type TemplateDescriptor struct { Variant2 string // contextual variant, e.g. "id" in render. // Misc. - LayoutMustMatch bool // If set, we only look for the exact layout. - IsPlainText bool // Whether this is a plain text template. + LayoutFromUserMustMatch bool // If set, we only look for the exact layout. + IsPlainText bool // Whether this is a plain text template. } func (d *TemplateDescriptor) normalizeFromFile() { - // fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat) - - if d.Layout == d.OutputFormat { - d.Layout = "" + if d.LayoutFromTemplate == d.OutputFormat { + d.LayoutFromTemplate = "" } if d.Kind == kinds.KindTemporary { d.Kind = "" } - if d.Layout == d.Kind { - d.Layout = "" + if d.LayoutFromTemplate == d.Kind { + d.LayoutFromTemplate = "" } } @@ -60,12 +59,13 @@ type descriptorHandler struct { // Note that this in this setup is usually a descriptor constructed from a page, // so we want to find the best match for that page. -func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor) weight { - if this.LayoutMustMatch && this.Layout != other.Layout { +func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool, this, other TemplateDescriptor) weight { + if this.LayoutFromUserMustMatch && this.LayoutFromUser != other.LayoutFromTemplate { return weightNoMatch } - w := this.doCompare(category, other) + w := this.doCompare(category, isEmbedded, s.opts.DefaultContentLanguage, other) + if w.w1 <= 0 { if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") { // See issue 13242. @@ -82,7 +82,7 @@ func (s descriptorHandler) compareDescriptors(category Category, this, other Tem } //lint:ignore ST1006 this vs other makes it easier to reason about. -func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor) weight { +func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, defaultContentLanguage string, other TemplateDescriptor) weight { w := weightNoMatch // HTML in plain text is OK, but not the other way around. @@ -92,22 +92,15 @@ func (this TemplateDescriptor) doCompare(category Category, other TemplateDescri if other.Kind != "" && other.Kind != this.Kind { return w } - if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout { - if isLayoutCustom(this.Layout) { - if this.Kind == "" { - this.Layout = "" - } else if this.Kind == kinds.KindPage { - this.Layout = layoutSingle - } else { - this.Layout = layoutList + + if other.LayoutFromTemplate != "" && other.LayoutFromTemplate != layoutAll { + if this.LayoutFromUser == "" || this.LayoutFromUser != other.LayoutFromTemplate { + if other.LayoutFromTemplate != this.LayoutFromTemplate { + return w } } - - // Test again. - if other.Layout != this.Layout { - return w - } } + if other.Lang != "" && other.Lang != this.Lang { return w } @@ -120,33 +113,43 @@ func (this TemplateDescriptor) doCompare(category Category, other TemplateDescri // We want e.g. home page in amp output format (media type text/html) to // find a template even if one isn't specified for that output format, // when one exist for the html output format (same media type). - if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && this.Layout != other.Layout)) { + skip := category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && (this.LayoutFromTemplate != other.LayoutFromTemplate && other.LayoutFromTemplate != layoutAll))) + if this.LayoutFromUser != "" { + skip = skip && (this.LayoutFromUser != other.LayoutFromTemplate) + } + if skip { return w } // Continue. } - // One example of variant1 and 2 is for render codeblocks: - // variant1=codeblock, variant2=go (language). - if other.Variant1 != "" && other.Variant1 != this.Variant1 { + if other.MediaType != this.MediaType { return w } - // If both are set and different, no match. - if other.Variant2 != "" && this.Variant2 != "" && other.Variant2 != this.Variant2 { - return w + // One example of variant1 and 2 is for render codeblocks: + // variant1=codeblock, variant2=go (language). + if other.Variant1 != "" { + if other.Variant1 != this.Variant1 { + return w + } + + if other.Variant2 != "" && other.Variant2 != this.Variant2 { + return w + } } const ( - weightKind = 3 // page, home, section, taxonomy, term (and only those) - weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter) - weightLayout = 2 // standard layouts (single,list,all) - weightOutputFormat = 2 // a configured output format (e.g. rss, html, json) - weightMediaType = 1 // a configured media type (e.g. text/html, text/plain) - weightLang = 1 // a configured language (e.g. en, nn, fr, ...) - weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image" - weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks. + weightKind = 5 // page, home, section, taxonomy, term (and only those) + weightcustomLayout = 6 // custom layout (mylayout, set in e.g. front matter) + weightLayoutStandard = 4 // standard layouts (single,list) + weightLayoutAll = 2 // the "all" layout + weightOutputFormat = 4 // a configured output format (e.g. rss, html, json) + weightMediaType = 1 // a configured media type (e.g. text/html, text/plain) + weightLang = 1 // a configured language (e.g. en, nn, fr, ...) + weightVariant1 = 6 // currently used for render hooks, e.g. "link", "image" + weightVariant2 = 4 // currently used for render hooks, e.g. the language "go" in code blocks. // We will use the values for group 2 and 3 // if the distance up to the template is shorter than @@ -170,17 +173,21 @@ func (this TemplateDescriptor) doCompare(category Category, other TemplateDescri w.w2 = weight2Group1 } - if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll { - if isLayoutCustom(this.Layout) { - w.w1 += weightcustomLayout - w.w2 = weight2Group2 - } else { - w.w1 += weightLayout - w.w2 = weight2Group1 - } + if other.LayoutFromTemplate != "" && (other.LayoutFromTemplate == this.LayoutFromTemplate) { + w.w1 += weightLayoutStandard + w.w2 = weight2Group1 + } else if other.LayoutFromTemplate == layoutAll { + w.w1 += weightLayoutAll + w.w2 = weight2Group1 } - if other.Lang != "" && other.Lang == this.Lang { + // LayoutCustom is only set in this (usually from Page.Layout). + if this.LayoutFromUser != "" && this.LayoutFromUser == other.LayoutFromTemplate { + w.w1 += weightcustomLayout + w.w2 = weight2Group2 + } + + if (other.Lang != "" && other.Lang == this.Lang) || (other.Lang == "" && this.Lang == defaultContentLanguage) { w.w1 += weightLang w.w3 += weight3 } @@ -199,12 +206,9 @@ func (this TemplateDescriptor) doCompare(category Category, other TemplateDescri w.w1 += weightVariant1 } - if other.Variant2 != "" && other.Variant2 == this.Variant2 { + if other.Variant1 != "" && other.Variant2 == this.Variant2 { w.w1 += weightVariant2 } - if other.Variant2 != "" && this.Variant2 == "" { - w.w1-- - } return w } diff --git a/tpl/tplimpl/templatedescriptor_test.go b/tpl/tplimpl/templatedescriptor_test.go index 76201287d..20ab47fba 100644 --- a/tpl/tplimpl/templatedescriptor_test.go +++ b/tpl/tplimpl/templatedescriptor_test.go @@ -20,14 +20,14 @@ func TestTemplateDescriptorCompare(t *testing.T) { less := func(category Category, this, other1, other2 TemplateDescriptor) { c.Helper() - result1 := dh.compareDescriptors(category, this, other1) - result2 := dh.compareDescriptors(category, this, other2) + result1 := dh.compareDescriptors(category, false, this, other1) + result2 := dh.compareDescriptors(category, false, this, other2) c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2)) } check := func(category Category, this, other TemplateDescriptor, less bool) { c.Helper() - result := dh.compareDescriptors(category, this, other) + result := dh.compareDescriptors(category, false, this, other) if less { c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result)) } else { @@ -38,29 +38,29 @@ func TestTemplateDescriptorCompare(t *testing.T) { check( CategoryBaseof, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"}, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "404", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "html", MediaType: "text/html"}, false, ) check( CategoryLayout, TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"}, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"}, true, ) less( CategoryLayout, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"}, - TemplateDescriptor{Layout: "list", OutputFormat: "html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html"}, + TemplateDescriptor{LayoutFromTemplate: "list", OutputFormat: "html"}, TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"}, ) check( CategoryLayout, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"}, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html", MediaType: "text/html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "myformat", MediaType: "text/html"}, false, ) } @@ -78,27 +78,27 @@ func BenchmarkCompareDescriptors(b *testing.B) { d1, d2 TemplateDescriptor }{ { - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, } b.ResetTimer() for i := 0; i < b.N; i++ { for _, pair := range pairs { - _ = dh.compareDescriptors(CategoryLayout, pair.d1, pair.d2) + _ = dh.compareDescriptors(CategoryLayout, false, pair.d1, pair.d2) } } } diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go index 1f911b9a5..19de48e38 100644 --- a/tpl/tplimpl/templates.go +++ b/tpl/tplimpl/templates.go @@ -3,7 +3,9 @@ package tplimpl import ( "io" "regexp" + "strconv" "strings" + "sync/atomic" "unicode" "unicode/utf8" @@ -42,18 +44,22 @@ var embeddedTemplatesAliases = map[string][]string{ "_shortcodes/twitter.html": {"_shortcodes/tweet.html"}, } -func (t *templateNamespace) parseTemplate(ti *TemplInfo) error { +func (s *TemplateStore) parseTemplate(ti *TemplInfo) error { + err := s.tns.doParseTemplate(ti) + if err != nil { + return s.addFileContext(ti, "parse of template failed", err) + } + + return err +} + +func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error { if !ti.noBaseOf || ti.category == CategoryBaseof { // Delay parsing until we have the base template. return nil } pi := ti.PathInfo name := pi.PathNoLeadingSlash() - if ti.isLegacyMapped { - // When mapping the old taxonomy structure to the new one, we may map the same path to multiple templates per kind. - // Append the kind here to make the name unique. - name += ("-" + ti.D.Kind) - } var ( templ tpl.Template @@ -62,12 +68,18 @@ func (t *templateNamespace) parseTemplate(ti *TemplInfo) error { if ti.D.IsPlainText { prototype := t.parseText + if prototype.Lookup(name) != nil { + name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10) + } templ, err = prototype.New(name).Parse(ti.content) if err != nil { return err } } else { prototype := t.parseHTML + if prototype.Lookup(name) != nil { + name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10) + } templ, err = prototype.New(name).Parse(ti.content) if err != nil { return err @@ -96,7 +108,14 @@ func (t *templateNamespace) parseTemplate(ti *TemplInfo) error { return err } } + } + // Issue #13599. + if ti.category == CategoryPartial && ti.Fi != nil && ti.Fi.Meta().PathInfo.Section() == "partials" { + aliasName := strings.TrimPrefix(name, "_") + if _, err := prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree); err != nil { + return err + } } } @@ -296,6 +315,8 @@ type templateNamespace struct { prototypeText *texttemplate.Template prototypeHTML *htmltemplate.Template + nameCounter atomic.Uint64 + standaloneText *texttemplate.Template baseofTextClones []*texttemplate.Template diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index eee962053..e6cd49c66 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -1,3 +1,18 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// 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 tplimpl import ( @@ -107,6 +122,7 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) { treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](), treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](), templatesByPath: maps.NewCache[string, *TemplInfo](), + shortcodesByName: maps.NewCache[string, *TemplInfo](), cacheLookupPartials: maps.NewCache[string, *TemplInfo](), // Note that the funcs passed below is just for name validation. @@ -301,7 +317,7 @@ func (ti *TemplInfo) String() string { return ti.PathInfo.String() } -func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) { +func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, d1 TemplateDescriptor, k1 string, slashCountK1 int, best *bestMatch) { if ti.baseVariants == nil { return } @@ -310,11 +326,11 @@ func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCount slashCountK2 := strings.Count(k2, "/") distance := slashCountK1 - slashCountK2 - for d, vv := range v { - weight := s.dh.compareDescriptors(CategoryBaseof, ti.D, d) + for d2, vv := range v { + weight := s.dh.compareDescriptors(CategoryBaseof, false, d1, d2) weight.distance = distance if best.isBetter(weight, vv.Template) { - best.updateValues(weight, k2, d, vv.Template) + best.updateValues(weight, k2, d2, vv.Template) } } return false, nil @@ -378,11 +394,11 @@ func (q *TemplateQuery) init() { } else if kinds.GetKindMain(q.Desc.Kind) == "" { q.Desc.Kind = "" } - if q.Desc.Layout == "" && q.Desc.Kind != "" { + if q.Desc.LayoutFromTemplate == "" && q.Desc.Kind != "" { if q.Desc.Kind == kinds.KindPage { - q.Desc.Layout = layoutSingle + q.Desc.LayoutFromTemplate = layoutSingle } else { - q.Desc.Layout = layoutList + q.Desc.LayoutFromTemplate = layoutList } } @@ -404,9 +420,10 @@ type TemplateStore struct { siteOpts SiteOptions htmlFormat output.Format - treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] - treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] - templatesByPath *maps.Cache[string, *TemplInfo] + treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] + treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] + templatesByPath *maps.Cache[string, *TemplInfo] + shortcodesByName *maps.Cache[string, *TemplInfo] dh descriptorHandler @@ -447,7 +464,7 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te continue } - if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, descBaseof, vv.D).w1 > 0 { + if vv.D.isKindInLayout(desc.LayoutFromTemplate) && s.dh.compareDescriptors(CategoryBaseof, false, descBaseof, vv.D).w1 > 0 { result = append(result, keyTemplateInfo{Key: k, Info: vv}) } } @@ -467,20 +484,31 @@ func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, w templ := ti.Template + parent := tpl.Context.CurrentTemplate.Get(ctx) + var level int + if parent != nil { + level = parent.Level + 1 + } currentTi := &tpl.CurrentTemplateInfo{ - Parent: tpl.Context.CurrentTemplate.Get(ctx), + Parent: parent, + Level: level, CurrentTemplateInfoOps: ti, } ctx = tpl.Context.CurrentTemplate.Set(ctx, currentTi) + const levelThreshold = 999 + if level > levelThreshold { + return fmt.Errorf("maximum template call stack size exceeded in %q", ti.Filename()) + } + if t.opts.Metrics != nil { defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now()) } execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data) if execErr != nil { - return t.addFileContext(ti, execErr) + return t.addFileContext(ti, "execute of template failed", execErr) } return nil } @@ -538,7 +566,7 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo { return m } best1.reset() - m.findBestMatchBaseof(s, key, slashCountKey, best1) + m.findBestMatchBaseof(s, q.Desc, key, slashCountKey, best1) if best1.w.w1 <= 0 { return nil } @@ -547,20 +575,36 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo { func (s *TemplateStore) LookupPartial(pth string) *TemplInfo { ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) { - d := s.templateDescriptorFromPath(pth) - desc := d.Desc - if desc.Layout != "" { - panic("shortcode template descriptor must not have a layout") + pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth).ForType(paths.TypePartial) + k1, _, _, desc, err := s.toKeyCategoryAndDescriptor(pi) + if err != nil { + return nil, err } + if desc.OutputFormat == "" && desc.MediaType == "" { + // Assume HTML. + desc.OutputFormat = s.htmlFormat.Name + desc.MediaType = s.htmlFormat.MediaType.Type + desc.IsPlainText = s.htmlFormat.IsPlainText + } + best := s.getBest() defer s.putBest(best) - s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best) + s.findBestMatchGet(s.key(path.Join(containerPartials, k1)), CategoryPartial, nil, desc, best) return best.templ, nil }) return ti } +func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo { + name = strings.ToLower(name) + ti, _ := s.shortcodesByName.Get(name) + if ti == nil { + return nil + } + return ti +} + func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo { q.init() k1 := s.key(q.Path) @@ -584,7 +628,7 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo { continue } - weight := s.dh.compareDescriptors(q.Category, q.Desc, k) + weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k) weight.distance = distance if best.isBetter(weight, vv) { best.updateValues(weight, k2, k, vv) @@ -610,7 +654,7 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer return } s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ") - ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s) + ts := fmt.Sprintf("kind: %q layout: %q lang: %q content: %.30s", vv.D.Kind, vv.D.LayoutFromTemplate, vv.D.Lang, s) fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts) } s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) { @@ -690,20 +734,12 @@ func (t *TemplateStore) UnusedTemplates() []*TemplInfo { var unused []*TemplInfo for vv := range t.templates() { - if vv.subCategory != SubCategoryMain { + if vv.subCategory != SubCategoryMain || vv.isLegacyMapped { // Skip inline partials and internal templates. continue } - if vv.noBaseOf { - if vv.executionCounter.Load() == 0 { - unused = append(unused, vv) - } - } else { - for vvv := range vv.BaseVariantsSeq() { - if vvv.Template.executionCounter.Load() == 0 { - unused = append(unused, vvv.Template) - } - } + if vv.executionCounter.Load() == 0 { + unused = append(unused, vv) } } @@ -737,7 +773,7 @@ func (s *TemplateStore) findBestMatchGet(key string, category Category, consider continue } - weight := s.dh.compareDescriptors(category, desc, k.d) + weight := s.dh.compareDescriptors(category, vv.subCategory == SubCategoryEmbedded, desc, k.d) if best.isBetter(weight, vv) { best.updateValues(weight, key, k.d, vv) } @@ -758,7 +794,7 @@ func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashC continue } - weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d) + weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k.d) weight.distance = distance isBetter := best.isBetter(weight, vv) @@ -807,7 +843,7 @@ func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *pa return nil } -func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error { +func (s *TemplateStore) addFileContext(ti *TemplInfo, what string, inerr error) error { if ti.Fi == nil { return inerr } @@ -839,25 +875,27 @@ func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error { fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename) fe.UpdateContent(f, lineMatcher) - if !fe.ErrorContext().Position.IsValid() { - return inErr, false - } - return fe, true + return fe, fe.ErrorContext().Position.IsValid() } - inerr = fmt.Errorf("execute of template failed: %w", inerr) + inerr = fmt.Errorf("%s: %w", what, inerr) - if err, ok := checkFilename(ti.Fi, inerr); ok { - return err + var ( + currentErr error + ok bool + ) + + if currentErr, ok = checkFilename(ti.Fi, inerr); ok { + return currentErr } if ti.base != nil { - if err, ok := checkFilename(ti.base.Fi, inerr); ok { - return err + if currentErr, ok = checkFilename(ti.base.Fi, inerr); ok { + return currentErr } } - return inerr + return currentErr } func (s *TemplateStore) extractIdentifiers(line string) []string { @@ -885,7 +923,7 @@ func (s *TemplateStore) extractInlinePartials() error { name := templ.Name() if !paths.HasExt(name) { // Assume HTML. This in line with how the lookup works. - name = name + ".html" + name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix } if !strings.HasPrefix(name, "_") { name = "_" + name @@ -1022,6 +1060,7 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, m1[d] = ti + s.shortcodesByName.Set(k2, ti) s.setTemplateByPath(pi.Path(), ti) if fi != nil { @@ -1063,6 +1102,12 @@ func (s *TemplateStore) insertTemplate2( panic("category not set") } + if category == CategoryPartial && d.OutputFormat == "" && d.MediaType == "" { + // See issue #13601. + d.OutputFormat = s.htmlFormat.Name + d.MediaType = s.htmlFormat.MediaType.Type + } + m := tree.Get(key) nk := nodeKey{c: category, d: d} @@ -1073,7 +1118,7 @@ func (s *TemplateStore) insertTemplate2( if !replace { if v, found := m[nk]; found { - if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) { + if len(pi.Identifiers()) >= len(v.PathInfo.Identifiers()) { // e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site. return nil, nil } @@ -1169,8 +1214,8 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo case containerPartials, containerShortcodes, containerMarkup: // OK. default: - applyLegacyMapping = true pi = fromLegacyPath(pi) + applyLegacyMapping = strings.Count(pi.Path(), "/") <= 2 } if applyLegacyMapping { @@ -1183,6 +1228,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo ext: pi.Ext(), outputFormat: pi.OutputFormat(), } + if m2, ok := legacyOrdinalMappings[key]; ok { if m1.ordinal < m2.m.ordinal { // Higher up == better match. @@ -1207,27 +1253,78 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo ) base := piOrig.PathBeforeLangAndOutputFormatAndExt() - identifiers := pi.IdentifiersUnknown() - - // Tokens on e.g. form /SECTIONKIND/THESECTION - insertSectionTokens := func(section string, kindOnly bool) string { - s := base - if !kindOnly { - s = strings.Replace(s, section, sectionToken, 1) - } - s = strings.Replace(s, kinds.KindSection, sectionKindToken, 1) - return s + identifiers := []string{} + if pi.Layout() != "" { + identifiers = append(identifiers, pi.Layout()) + } + if pi.Kind() != "" { + identifiers = append(identifiers, pi.Kind()) } - for _, section := range identifiers { - if section == baseNameBaseof { + shouldIncludeSection := func(section string) bool { + switch section { + case containerShortcodes, containerPartials, containerMarkup: + return false + case "taxonomy", "": + return false + default: + for k, v := range s.opts.TaxonomySingularPlural { + if k == section || v == section { + return false + } + } + return true + } + } + if shouldIncludeSection(pi.Section()) { + identifiers = append(identifiers, pi.Section()) + } + + identifiers = helpers.UniqueStrings(identifiers) + + // Tokens on e.g. form /SECTIONKIND/THESECTION + insertSectionTokens := func(section string) []string { + kindOnly := isLayoutStandard(section) + var ss []string + s1 := base + if !kindOnly { + s1 = strings.ReplaceAll(s1, section, sectionToken) + } + s1 = strings.ReplaceAll(s1, kinds.KindSection, sectionKindToken) + if s1 != base { + ss = append(ss, s1) + } + s1 = strings.ReplaceAll(base, kinds.KindSection, sectionKindToken) + if !kindOnly { + s1 = strings.ReplaceAll(s1, section, sectionToken) + } + if s1 != base { + ss = append(ss, s1) + } + + helpers.UniqueStringsReuse(ss) + + return ss + } + + for _, id := range identifiers { + if id == "" { continue } - kindOnly := isLayoutStandard(section) - p := insertSectionTokens(section, kindOnly) - if m1, ok := s.opts.legacyMappingSection[p]; ok { - m1.mapping.targetPath = strings.Replace(m1.mapping.targetPath, sectionToken, section, 1) - handleMapping(m1) + + p := insertSectionTokens(id) + for _, ss := range p { + if m1, ok := s.opts.legacyMappingSection[ss]; ok { + targetPath := m1.mapping.targetPath + + if targetPath != "" { + targetPath = strings.ReplaceAll(targetPath, sectionToken, id) + targetPath = strings.ReplaceAll(targetPath, sectionKindToken, id) + targetPath = strings.ReplaceAll(targetPath, "//", "/") + } + m1.mapping.targetPath = targetPath + handleMapping(m1) + } } } @@ -1325,7 +1422,7 @@ func (s *TemplateStore) parseTemplates() error { if vv.state == processingStateTransformed { continue } - if err := s.tns.parseTemplate(vv); err != nil { + if err := s.parseTemplate(vv); err != nil { return err } } @@ -1345,7 +1442,7 @@ func (s *TemplateStore) parseTemplates() error { // The regular expression used to detect if a template needs a base template has some // rare false positives. Assume we don't need one. vv.noBaseOf = true - if err := s.tns.parseTemplate(vv); err != nil { + if err := s.parseTemplate(vv); err != nil { return err } continue @@ -1374,7 +1471,7 @@ func (s *TemplateStore) parseTemplates() error { if vvv.state == processingStateTransformed { continue } - if err := s.tns.parseTemplate(vvv); err != nil { + if err := s.parseTemplate(vvv); err != nil { return err } } @@ -1402,43 +1499,6 @@ type PathTemplateDescriptor struct { Desc TemplateDescriptor } -// templateDescriptorFromPath returns a template descriptor from the given path. -// This is currently used in partial lookups only. -func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor { - var ( - mt media.Type - of output.Format - ) - - // Common cases. - dotCount := strings.Count(pth, ".") - if dotCount <= 1 { - if dotCount == 0 { - // Asume HTML. - of, mt = s.resolveOutputFormatAndOrMediaType("html", "") - } else { - pth = strings.TrimPrefix(pth, "/") - ext := path.Ext(pth) - pth = strings.TrimSuffix(pth, ext) - ext = ext[1:] - of, mt = s.resolveOutputFormatAndOrMediaType("", ext) - } - } else { - path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth) - pth = path.PathNoIdentifier() - of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext()) - } - - return PathTemplateDescriptor{ - Path: pth, - Desc: TemplateDescriptor{ - OutputFormat: of.Name, - MediaType: mt.Type, - IsPlainText: of.IsPlainText, - }, - } -} - // resolveOutputFormatAndOrMediaType resolves the output format and/or media type // based on the given output format suffix and media type suffix. // Either of the suffixes can be empty, and the function will try to find a match @@ -1475,6 +1535,9 @@ func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (outp return outputFormat, mediaType } +// templates iterates over all templates in the store. +// Note that for templates with one or more base templates applied, +// we will yield the variants, e.g. the templates that's actually in use. func (s *TemplateStore) templates() iter.Seq[*TemplInfo] { return func(yield func(*TemplInfo) bool) { for _, v := range s.treeMain.All() { @@ -1511,25 +1574,13 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext()) nameNoIdentifier := p.NameNoIdentifier() - var layout string - unknownids := p.IdentifiersUnknown() - if p.Type() == paths.TypeShortcode { - if len(unknownids) > 1 { - // The name is the last identifier. - layout = unknownids[len(unknownids)-2] - } - } else if len(unknownids) > 0 { - // Pick the last, closest to the base name. - layout = unknownids[len(unknownids)-1] - } - d := TemplateDescriptor{ - Lang: p.Lang(), - OutputFormat: p.OutputFormat(), - MediaType: mediaType.Type, - Kind: p.Kind(), - Layout: layout, - IsPlainText: outputFormat.IsPlainText, + Lang: p.Lang(), + OutputFormat: p.OutputFormat(), + MediaType: mediaType.Type, + Kind: p.Kind(), + LayoutFromTemplate: p.Layout(), + IsPlainText: outputFormat.IsPlainText, } d.normalizeFromFile() @@ -1562,12 +1613,13 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin } if category == CategoryPartial { - d.Layout = "" + d.LayoutFromTemplate = "" k1 = p.PathNoIdentifier() } if category == CategoryShortcode { k1 = p.PathNoIdentifier() + parts := strings.Split(k1, "/"+containerShortcodes+"/") k1 = parts[0] if len(parts) > 1 { @@ -1577,15 +1629,15 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin } // Legacy layout for home page. - if d.Layout == "index" { + if d.LayoutFromTemplate == "index" { if d.Kind == "" { d.Kind = kinds.KindHome } - d.Layout = "" + d.LayoutFromTemplate = "" } - if d.Layout == d.Kind { - d.Layout = "" + if d.LayoutFromTemplate == d.Kind { + d.LayoutFromTemplate = "" } k1 = strings.TrimPrefix(k1, "/_default") @@ -1596,7 +1648,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin if category == CategoryMarkup { // We store all template nodes for a given directory on the same level. k1 = strings.TrimSuffix(k1, "/_markup") - parts := strings.Split(d.Layout, "-") + parts := strings.Split(d.LayoutFromTemplate, "-") if len(parts) < 2 { return "", "", 0, TemplateDescriptor{}, fmt.Errorf("unrecognized render hook template") } @@ -1605,7 +1657,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin if len(parts) > 2 { d.Variant2 = parts[2] } - d.Layout = "" // This allows using page layout as part of the key for lookups. + d.LayoutFromTemplate = "" // This allows using page layout as part of the key for lookups. } return k1, k2, category, d, nil @@ -1640,30 +1692,14 @@ func (s *TemplateStore) transformTemplates() error { if vv.category == CategoryBaseof { continue } - if !vv.noBaseOf { - for vvv := range vv.BaseVariantsSeq() { - tctx, err := applyTemplateTransformers(vvv.Template, lookup) - if err != nil { - return err - } - - for name, node := range tctx.deferNodes { - if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil { - return err - } - } - } - } else { - tctx, err := applyTemplateTransformers(vv, lookup) - if err != nil { + tctx, err := applyTemplateTransformers(vv, lookup) + if err != nil { + return err + } + for name, node := range tctx.deferNodes { + if err := s.addDeferredTemplate(vv, name, node); err != nil { return err } - - for name, node := range tctx.deferNodes { - if err := s.addDeferredTemplate(vv, name, node); err != nil { - return err - } - } } } @@ -1776,14 +1812,19 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool { // Anything is better than nothing. return true } + if w.w1 <= 0 { + if best.w.w1 <= 0 { return ti.PathInfo.Path() < best.templ.PathInfo.Path() } return false } - if best.w.w1 > 0 { + // Note that for render hook templates, we need to make + // the embedded render hook template wih if they're a better match, + // e.g. render-codeblock-goat.html. + if best.templ.category != CategoryMarkup && best.w.w1 > 0 { currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded if currentBestIsEmbedded { if ti.subCategory != SubCategoryEmbedded { @@ -1816,11 +1857,7 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool { return true } - if ti.D.Layout != "" && best.desc.Layout != "" { - return ti.D.Layout != layoutAll - } - - return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path() + return ti.PathInfo.Path() < best.templ.PathInfo.Path() } return true @@ -1868,17 +1905,6 @@ type weight struct { distance int } -func (w weight) isEqualWeights(other weight) bool { - return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3 -} - -func isLayoutCustom(s string) bool { - if s == "" || isLayoutStandard(s) { - return false - } - return true -} - func isLayoutStandard(s string) bool { switch s { case layoutAll, layoutList, layoutSingle: @@ -1888,6 +1914,10 @@ func isLayoutStandard(s string) bool { } } +func (w weight) isEqualWeights(other weight) bool { + return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3 +} + func configureSiteStorage(opts SiteOptions, watching bool) *storeSite { funcsv := make(map[string]reflect.Value) diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go index 4f76626ad..638341581 100644 --- a/tpl/tplimpl/templatestore_integration_test.go +++ b/tpl/tplimpl/templatestore_integration_test.go @@ -3,6 +3,7 @@ package tplimpl_test import ( "context" "io" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -441,7 +442,7 @@ title: "P1" {{ define "main" }}FOO{{ end }} -- layouts/_default/single.json -- -- layouts/_default/single.html -- -{{ define "main" }}MAIN{{ end }} +{{ define "main" }}MAIN /_default/single.html{{ end }} -- layouts/post/single.html -- {{ define "main" }}MAIN{{ end }} -- layouts/_partials/usedpartial.html -- @@ -461,6 +462,8 @@ title: "P1" ) b.Build() + b.AssertFileContent("public/p1/index.html", "MAIN /_default/single.html") + unused := b.H.GetTemplateStore().UnusedTemplates() var names []string for _, tmpl := range unused { @@ -468,8 +471,9 @@ title: "P1" names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash()) } } - b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names)) + b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"}) + b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names)) } func TestCreateManyTemplateStores(t *testing.T) { @@ -507,7 +511,7 @@ baseof: {{ block "main" . }}{{ end }} q := tplimpl.TemplateQuery{ Path: "/baz", Category: tplimpl.CategoryLayout, - Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"}, + Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"}, } for i := 0; i < b.N; i++ { store.LookupPagesLayout(q) @@ -518,7 +522,7 @@ baseof: {{ block "main" . }}{{ end }} q := tplimpl.TemplateQuery{ Path: "/foo/bar", Category: tplimpl.CategoryLayout, - Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"}, + Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"}, } for i := 0; i < b.N; i++ { store.LookupPagesLayout(q) @@ -645,9 +649,6 @@ layout: mylayout b := hugolib.Test(t, files, hugolib.TestOptWarn()) - // s := b.H.Sites[0].TemplateStore - // s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout) - b.AssertLogContains("! WARN") // Single pages. @@ -684,6 +685,102 @@ layout: mylayout b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml") } +func TestLookupOrderIssue13636(t *testing.T) { + t.Parallel() + + filesTemplate := ` +-- hugo.toml -- +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +[languages.nn] +weight = 2 +-- content/s1/p1.en.md -- +--- +outputs: ["html", "amp", "json"] +--- +-- content/s1/p1.nn.md -- +--- +outputs: ["html", "amp", "json"] +--- +-- layouts/L1 -- +L1 +-- layouts/L2 -- +L2 +-- layouts/L3 -- +L3 + +` + + tests := []struct { + Lang string + L1 string + L2 string + L3 string + ExpectHTML string + ExpectAmp string + ExpectJSON string + }{ + {"en", "all.en.html", "all.html", "single.html", "single.html", "single.html", ""}, + {"en", "all.amp.html", "all.html", "page.html", "page.html", "all.amp.html", ""}, + {"en", "all.amp.html", "all.html", "list.html", "all.html", "all.amp.html", ""}, + {"en", "all.en.html", "all.json", "single.html", "single.html", "single.html", "all.json"}, + {"en", "all.en.html", "single.json", "single.html", "single.html", "single.html", "single.json"}, + {"en", "all.en.html", "all.html", "list.html", "all.en.html", "all.en.html", ""}, + {"en", "list.en.html", "list.html", "list.en.html", "", "", ""}, + {"nn", "all.en.html", "all.html", "single.html", "single.html", "single.html", ""}, + {"nn", "all.en.html", "all.nn.html", "single.html", "single.html", "single.html", ""}, + {"nn", "all.en.html", "all.nn.html", "single.nn.html", "single.nn.html", "single.nn.html", ""}, + {"nn", "single.json", "single.nn.json", "all.json", "", "", "single.nn.json"}, + {"nn", "single.json", "single.en.json", "all.nn.json", "", "", "single.json"}, + } + + for i, test := range tests { + if i != 8 { + // continue + } + files := strings.ReplaceAll(filesTemplate, "L1", test.L1) + files = strings.ReplaceAll(files, "L2", test.L2) + files = strings.ReplaceAll(files, "L3", test.L3) + t.Logf("Test %d: %s %s %s %s", i, test.Lang, test.L1, test.L2, test.L3) + + for range 3 { + b := hugolib.Test(t, files) + b.Assert(len(b.H.Sites), qt.Equals, 2) + + var ( + pubhHTML = "public/LANG/s1/p1/index.html" + pubhAmp = "public/LANG/amp/s1/p1/index.html" + pubhJSON = "public/LANG/s1/p1/index.json" + ) + + pubhHTML = strings.ReplaceAll(pubhHTML, "LANG", test.Lang) + pubhAmp = strings.ReplaceAll(pubhAmp, "LANG", test.Lang) + pubhJSON = strings.ReplaceAll(pubhJSON, "LANG", test.Lang) + + if test.ExpectHTML != "" { + b.AssertFileContent(pubhHTML, test.ExpectHTML) + } else { + b.AssertFileExists(pubhHTML, false) + } + + if test.ExpectAmp != "" { + b.AssertFileContent(pubhAmp, test.ExpectAmp) + } else { + b.AssertFileExists(pubhAmp, false) + } + + if test.ExpectJSON != "" { + b.AssertFileContent(pubhJSON, test.ExpectJSON) + } else { + b.AssertFileExists(pubhJSON, false) + } + } + } +} + func TestLookupShortcodeDepth(t *testing.T) { t.Parallel() @@ -823,6 +920,153 @@ func TestPartialHTML(t *testing.T) { b.AssertFileContent("public/index.html", "") } +// Issue #13593. +func TestGoatAndNoGoat(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/_index.md -- +--- +title: "Home" +--- + + +§§§ +printf "Hello, world!" +§§§ + + +§§§ goat +.---. .-. .-. .-. .---. +| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | +'---' '-' '+' '+' '---' +§§§ + + + +-- layouts/all.html -- +{{ .Content }} + +` + + b := hugolib.Test(t, files) + + // Basic code block. + b.AssertFileContent("public/index.html", "printf "Hello, world!"\n") + + // Goat code block. + b.AssertFileContent("public/index.html", "Menlo,Lucida") +} + +// Issue #13595. +func TestGoatAndNoGoatCustomTemplate(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/_index.md -- +--- +title: "Home" +--- + +§§§ +printf "Hello, world!" +§§§ + +§§§ goat +.---. .-. .-. .-. .---. +| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | +'---' '-' '+' '+' '---' +§§§ + + + +-- layouts/_markup/render-codeblock.html -- +_markup/render-codeblock.html +-- layouts/all.html -- +{{ .Content }} + +` + + b := hugolib.Test(t, files) + + // Basic code block. + b.AssertFileContent("public/index.html", "_markup/render-codeblock.html") + + // Goat code block. + b.AssertFileContent("public/index.html", "Menlo,Lucida") +} + +func TestGoatcustom(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- content/_index.md -- +--- +title: "Home" +--- + +§§§ +printf "Hello, world!" +§§§ + +§§§ goat +.---. .-. .-. .-. .---. +| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | +'---' '-' '+' '+' '---' +§§§ + + + +-- layouts/_markup/render-codeblock.html -- +_markup/render-codeblock.html +-- layouts/_markup/render-codeblock-goat.html -- +_markup/render-codeblock-goat.html +-- layouts/all.html -- +{{ .Content }} + +` + + b := hugolib.Test(t, files) + + // Basic code block. + b.AssertFileContent("public/index.html", "_markup/render-codeblock.html") + + // Custom Goat code block. + b.AssertFileContent("public/index.html", "_markup/render-codeblock.html_markup/render-codeblock-goat.html") +} + +func TestLookupCodeblockIssue13651(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/all.html -- +{{ .Content }}| +-- layouts/_markup/render-codeblock-foo.html -- +render-codeblock-foo.html +-- content/_index.md -- +--- +--- + +§§§ +printf "Hello, world!" +§§§ + +§§§ foo +printf "Hello, world again!" +§§§ +` + + b := hugolib.Test(t, files) + + content := b.FileContent("public/index.html") + fooCount := strings.Count(content, "render-codeblock-foo.html") + b.Assert(fooCount, qt.Equals, 1) +} + // Issue #13515 func TestPrintPathWarningOnDotRemoval(t *testing.T) { t.Parallel() @@ -841,7 +1085,7 @@ All. b := hugolib.Test(t, files, hugolib.TestOptWarn()) - b.AssertLogContains("Duplicate content path") + b.AssertLogContains("! Duplicate content path") } // Issue #13577. @@ -974,6 +1218,63 @@ s2. }) } +func TestStandardLayoutInFrontMatter13588(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','page','rss','sitemap','taxonomy','term'] +-- content/s1/_index.md -- +--- +title: s1 +--- +-- content/s2/_index.md -- +--- +title: s2 +layout: list +--- +-- content/s3/_index.md -- +--- +title: s3 +layout: single +--- +-- layouts/list.html -- +list.html +-- layouts/section.html -- +section.html +-- layouts/single.html -- +single.html +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/s1/index.html", "section.html") + b.AssertFileContent("public/s2/index.html", "list.html") // fail + b.AssertFileContent("public/s3/index.html", "single.html") // fail +} + +func TestIssue13605(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +-- content/s1/p1.md -- +--- +title: p1 +--- +{{< sc >}} +-- layouts/s1/_shortcodes/sc.html -- +layouts/s1/_shortcodes/sc.html +-- layouts/single.html -- +{{ .Content }} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/_shortcodes/sc.html") +} + func TestSkipDotFiles(t *testing.T) { t.Parallel() @@ -988,3 +1289,110 @@ All. // Just make sure it doesn't fail. hugolib.Test(t, files) } + +func TestPartialsLangIssue13612(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','section','sitemap','taxonomy','term'] + +defaultContentLanguage = 'ru' +defaultContentLanguageInSubdir = true + +[languages.ru] +weight = 1 + +[languages.en] +weight = 2 + +[outputs] +home = ['html','rss'] + +-- layouts/_partials/comment.en.html -- +layouts/_partials/comment.en.html +-- layouts/_partials/comment.en.xml -- +layouts/_partials/comment.en.xml +-- layouts/_partials/comment.ru.html -- +layouts/_partials/comment.ru.html +-- layouts/_partials/comment.ru.xml -- +layouts/_partials/comment.ru.xml +-- layouts/home.html -- +{{ partial (print "comment." (default "ru" .Lang) ".html") . }} +-- layouts/home.rss.xml -- +{{ partial (print "comment." (default "ru" .Lang) ".xml") . }} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/en/index.html", "layouts/_partials/comment.en.html") + b.AssertFileContent("public/en/index.xml", "layouts/_partials/comment.en.xml") // fail + b.AssertFileContent("public/ru/index.html", "layouts/_partials/comment.ru.html") // fail + b.AssertFileContent("public/ru/index.xml", "layouts/_partials/comment.ru.xml") // fail +} + +func TestLayoutIssue13628(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','sitemap','taxonomy','term'] +-- content/p1.md -- +--- +title: p1 +layout: foo +--- +-- layouts/single.html -- +layouts/single.html +-- layouts/list.html -- +layouts/list.html +` + + for range 5 { + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", "layouts/single.html") + } +} + +func TestTemplateLoop(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/_partials/p.html -- +p: {{ partial "p.html" . }} +-- layouts/all.html -- +{{ partial "p.html" . }} + +` + b, err := hugolib.TestE(t, files) + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, "error calling partial: maximum template call stack size exceeded") +} + +func TestIssue13630(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['rss','sitemap'] +-- content/p1.md -- +--- +title: p1 +layout: foo +--- +-- layouts/list.html -- +layouts/list.html +-- layouts/taxononmy.html.html -- +layouts/taxononmy.html.html +` + + var b *hugolib.IntegrationTestBuilder + + for range 3 { + b = hugolib.Test(t, files) + b.AssertFileExists("public/p1/index.html", false) + } +} diff --git a/tpl/tplimpl/templatetransform.go b/tpl/tplimpl/templatetransform.go index cba4c6584..eca9fdad1 100644 --- a/tpl/tplimpl/templatetransform.go +++ b/tpl/tplimpl/templatetransform.go @@ -175,6 +175,9 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err } case *parse.CommandNode: + if x == nil { + return true, nil + } c.collectInner(x) keep := c.collectReturnNode(x) diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go index 8b80d5b60..b62898923 100644 --- a/tpl/tplimpl/tplimpl_integration_test.go +++ b/tpl/tplimpl/tplimpl_integration_test.go @@ -1,3 +1,18 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// 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 tplimpl_test import ( diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go index ceb80309b..2b3c7d40e 100644 --- a/tpl/transform/transform_integration_test.go +++ b/tpl/transform/transform_integration_test.go @@ -379,3 +379,119 @@ Markdown: {{ $markdown }}| b.AssertFileContent("public/index.html", "Markdown: ## Heading 2\n|") } + +func TestUnmarshalCSV(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +-- layouts/all.html -- +{{ $opts := OPTS }} +{{ with resources.Get "pets.csv" | transform.Unmarshal $opts }} + {{ jsonify . }} +{{ end }} +-- assets/pets.csv -- +DATA +` + + // targetType = map + f := strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`) + f = strings.ReplaceAll(f, "DATA", + "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7", + ) + b := hugolib.Test(t, f) + b.AssertFileContent("public/index.html", + `[{"age":"3","breed":"Collie","name":"Spot","type":"dog"},{"age":"7","breed":"Malicious","name":"Felix","type":"cat"}]`, + ) + + // targetType = map (no data) + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`) + f = strings.ReplaceAll(f, "DATA", "") + b = hugolib.Test(t, f) + b.AssertFileContent("public/index.html", "") + + // targetType = slice + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "slice"`) + f = strings.ReplaceAll(f, "DATA", + "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7", + ) + b = hugolib.Test(t, f) + b.AssertFileContent("public/index.html", + `[["name","type","breed","age"],["Spot","dog","Collie","3"],["Felix","cat","Malicious","7"]]`, + ) + + // targetType = slice (no data) + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "slice"`) + f = strings.ReplaceAll(f, "DATA", "") + b = hugolib.Test(t, f) + b.AssertFileContent("public/index.html", "") + + // targetType not specified + f = strings.ReplaceAll(files, "OPTS", "dict") + f = strings.ReplaceAll(f, "DATA", + "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7", + ) + b = hugolib.Test(t, f) + b.AssertFileContent("public/index.html", + `[["name","type","breed","age"],["Spot","dog","Collie","3"],["Felix","cat","Malicious","7"]]`, + ) + + // targetType not specified (no data) + f = strings.ReplaceAll(files, "OPTS", "dict") + f = strings.ReplaceAll(f, "DATA", "") + b = hugolib.Test(t, f) + b.AssertFileContent("public/index.html", "") + + // targetType = foo + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "foo"`) + _, err := hugolib.TestE(t, f) + if err == nil { + t.Errorf("expected error") + } else { + if !strings.Contains(err.Error(), `invalid targetType: expected either slice or map, received foo`) { + t.Log(err.Error()) + t.Errorf("error message does not match expected error message") + } + } + + // targetType = foo (no data) + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "foo"`) + f = strings.ReplaceAll(f, "DATA", "") + _, err = hugolib.TestE(t, f) + if err == nil { + t.Errorf("expected error") + } else { + if !strings.Contains(err.Error(), `invalid targetType: expected either slice or map, received foo`) { + t.Log(err.Error()) + t.Errorf("error message does not match expected error message") + } + } + + // targetType = map (error: expected at least a header row and one data row) + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`) + _, err = hugolib.TestE(t, f) + if err == nil { + t.Errorf("expected error") + } else { + if !strings.Contains(err.Error(), `expected at least a header row and one data row`) { + t.Log(err.Error()) + t.Errorf("error message does not match expected error message") + } + } + + // targetType = map (error: header row contains duplicate field names) + f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`) + f = strings.ReplaceAll(f, "DATA", + "name,name,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7", + ) + _, err = hugolib.TestE(t, f) + if err == nil { + t.Errorf("expected error") + } else { + if !strings.Contains(err.Error(), `header row contains duplicate field names`) { + t.Log(err.Error()) + t.Errorf("error message does not match expected error message") + } + } +} diff --git a/watchtestscripts.sh b/watchtestscripts.sh index bf61d0cc3..5c6f90009 100755 --- a/watchtestscripts.sh +++ b/watchtestscripts.sh @@ -3,5 +3,5 @@ trap exit SIGINT # I use "run tests on save" in my editor. -# Unfortunately, changes to text files does not trigger this. Hence this workaround. -while true; do find testscripts -type f -name "*.txt" | entr -pd touch main_test.go; done \ No newline at end of file +# Unfortunately, changes to text files do not trigger this. Hence this workaround. +while true; do find testscripts -type f -name "*.txt" | entr -pd touch main_test.go; done