Revert the breaking change from 0.146.0 with dots in content filenames

Closes #13632
This commit is contained in:
Bjørn Erik Pedersen 2025-04-21 16:15:21 +02:00
parent 6d69dc88a4
commit 496730840e
8 changed files with 175 additions and 69 deletions

View file

@ -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
}
@ -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

View file

@ -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))
})
}

View file

@ -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
}

View file

@ -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)

View file

@ -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]

View file

@ -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, ""},

View file

@ -654,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.LayoutFromTemplate, 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) {
@ -1126,7 +1126,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
}
@ -1261,7 +1261,10 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
)
base := piOrig.PathBeforeLangAndOutputFormatAndExt()
identifiers := pi.IdentifiersUnknown()
identifiers := []string{}
if pi.Layout() != "" {
identifiers = append(identifiers, pi.Layout())
}
if pi.Kind() != "" {
identifiers = append(identifiers, pi.Kind())
}
@ -1576,24 +1579,12 @@ 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(),
LayoutFromTemplate: layout,
LayoutFromTemplate: p.Layout(),
IsPlainText: outputFormat.IsPlainText,
}
@ -1633,6 +1624,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin
if category == CategoryShortcode {
k1 = p.PathNoIdentifier()
parts := strings.Split(k1, "/"+containerShortcodes+"/")
k1 = parts[0]
if len(parts) > 1 {

View file

@ -1056,7 +1056,7 @@ All.
b := hugolib.Test(t, files, hugolib.TestOptWarn())
b.AssertLogContains("Duplicate content path")
b.AssertLogContains("! Duplicate content path")
}
// Issue #13577.