identifierCustomWrapper = "_"
)
+// Known prefixes for ._prefix_value_. identifiers.
+const (
+ prefixLanguage = "language_"
+ prefixVersion = "version_"
+ prefixRole = "role_"
+ prefixOutputFormat = "outputformat_"
+ prefixKind = "kind_"
+ prefixLayout = "layout_"
+)
+
// isCustomWrapperIdentifier tells whether a supplied path is of the form _xyz_.
// must have non-empty content between the identifierCustomWrapper's to pass.
func isCustomWrapperIdentifier(s string) bool {
strings.HasSuffix(s, identifierCustomWrapper)
}
+// parsePrefixIdentifier parses inner content of a wrapper block (with outer underscores stripped)
+// and returns the prefix. The prefix match is case-insensitive.
+// Returns empty string if not a known prefix.
+func parsePrefixIdentifier(inner string) string {
+ innerLower := strings.ToLower(inner)
+ for _, p := range []string{prefixLanguage, prefixVersion, prefixRole, prefixOutputFormat, prefixKind, prefixLayout} {
+ if strings.HasPrefix(innerLower, p) {
+ return p
+ }
+ }
+ return ""
+}
+
+// findWrapperDotPositions returns positions of dots inside ._..._. wrapper blocks.
+// These dots should be skipped during the main backward dot-scanning loop,
+// so that wrapper blocks are treated as single opaque segments.
+func findWrapperDotPositions(s string) []int {
+ lastSlash := strings.LastIndex(s, "/")
+ if lastSlash == -1 {
+ return nil
+ }
+
+ var skipDots []int
+ i := lastSlash
+ for i < len(s) {
+ // Look for ._ which could start a wrapper block.
+ if i+2 < len(s) && s[i] == '.' && s[i+1] == '_' {
+ // Find the closing _. or _ at end of string.
+ end := -1
+ for j := i + 2; j < len(s); j++ {
+ if s[j] == '/' {
+ break
+ }
+ if s[j] == '_' && (j+1 >= len(s) || s[j+1] == '.') {
+ end = j
+ break
+ }
+ }
+ if end != -1 {
+ // Record dot positions inside the wrapper block.
+ for j := i + 2; j < end; j++ {
+ if s[j] == '.' {
+ skipDots = append(skipDots, j)
+ }
+ }
+ i = end + 1
+ continue
+ }
+ }
+ i++
+ }
+ return skipDots
+}
+
+// isSkippedDot reports whether position i is in the sorted skipDots slice.
+func isSkippedDot(skipDots []int, i int) bool {
+ for _, d := range skipDots {
+ if d == i {
+ return true
+ }
+ if d > i {
+ break
+ }
+ }
+ return false
+}
+
+// applyPrefixIdentifier validates and stores a prefix identifier.
+// id is the value-only LowHigh (e.g. pointing at "v1.0.0" in p.s, not the full wrapper).
+// Returns true if the identifier was recognized and applied.
+func (pp *PathParser) applyPrefixIdentifier(component, prefix string, p *Path, id types.LowHigh[string]) bool {
+ value := strings.ToLower(p.s[id.Low:id.High])
+ switch prefix {
+ case prefixLanguage:
+ if pp.LanguageIndex != nil {
+ if _, ok := pp.LanguageIndex[value]; ok {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
+ return true
+ }
+ if pp.IsLangDisabled != nil && pp.IsLangDisabled(value) {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
+ p.disabled = true
+ return true
+ }
+ }
+ case prefixVersion:
+ if pp.ConfiguredDimensions != nil {
+ if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(value); idx >= 0 {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierVersions = append(p.posIdentifierVersions, len(p.identifiersKnown)-1)
+ return true
+ }
+ }
+ case prefixRole:
+ if pp.ConfiguredDimensions != nil {
+ if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(value); idx >= 0 {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierRoles = append(p.posIdentifierRoles, len(p.identifiersKnown)-1)
+ return true
+ }
+ }
+ case prefixOutputFormat:
+ if component == files.ComponentFolderLayouts && pp.IsOutputFormat != nil {
+ if pp.IsOutputFormat(value, "") {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierPrefixOutputFormat = len(p.identifiersKnown) - 1
+ return true
+ }
+ }
+ case prefixKind:
+ if component == files.ComponentFolderLayouts {
+ if kinds.GetKindMain(value) != "" {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierPrefixKind = len(p.identifiersKnown) - 1
+ return true
+ }
+ }
+ case prefixLayout:
+ if component == files.ComponentFolderLayouts {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierPrefixLayout = len(p.identifiersKnown) - 1
+ return true
+ }
+ }
+ return false
+}
+
// PathParser parses and manages paths.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
pp.init()
- lang := p.Lang()
- v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
+ langs := p.Langs()
+ versions := p.Versions()
+ roles := p.Roles()
+ lang := p.Lang() // First or dot-based language.
+ // Cache by the full site-selection identity derived from the path:
+ // languages, selected language, versions, and roles.
+ cacheKey := strings.Join(langs, ",") + "/" + lang + "/" + strings.Join(versions, ",") + "|" + strings.Join(roles, ",")
+ v, _ := pp.sitesMatrixCache.GetOrCreate(cacheKey, func() (sitesmatrix.VectorStore, error) {
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
- if lang != "" {
+ if len(langs) > 0 {
+ for _, l := range langs {
+ if idx, ok := pp.LanguageIndex[l]; ok {
+ builder.WithLanguageIndices(idx)
+ }
+ }
+ } else if lang != "" {
if idx, ok := pp.LanguageIndex[lang]; ok {
builder.WithLanguageIndices(idx)
}
}
+ for _, version := range versions {
+ if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(version); idx >= 0 {
+ builder.WithVersionIndices(idx)
+ }
+ }
+ for _, role := range roles {
+ if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(role); idx >= 0 {
+ builder.WithRoleIndices(idx)
+ }
+ }
switch p.Component() {
case files.ComponentFolderContent:
sid := p.s[id.Low:id.High]
if isCustomWrapperIdentifier(sid) {
- p.identifiersKnown = append(p.identifiersKnown, id)
- p.posIdentifierCustom = len(p.identifiersKnown) - 1
- found = true
+ inner := sid[1 : len(sid)-1]
+ if prefix := parsePrefixIdentifier(inner); prefix != "" {
+ // Value-only LowHigh: skip leading _ + prefix, trailing _
+ valueID := types.LowHigh[string]{Low: id.Low + 1 + len(prefix), High: id.High - 1}
+ if pp.applyPrefixIdentifier(component, prefix, p, valueID) {
+ found = true
+ }
+ }
+ if !found {
+ p.identifiersKnown = append(p.identifiersKnown, id)
+ p.posIdentifierCustom = len(p.identifiersKnown) - 1
+ found = true
+ }
}
- if len(p.identifiersKnown) == 0 {
+ if found {
+ // Already handled (e.g. prefix wrapper).
+ } else if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
}
}
+
+ if found {
+ if isLast {
+ // The isLast identifier starts right at the container boundary.
+ // Treat it as part of the name (e.g. layout name "list" in list.no.html).
+ if p.posNameHigh <= 0 {
+ p.posNameHigh = lastDot
+ }
+ } else {
+ p.posNameHigh = i // The '.' before this identifier.
+ }
+ }
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
p.s = s
+
+ // Find dots inside ._..._. wrapper blocks that must be skipped.
+ skipDots := findWrapperDotPositions(s)
+
slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
- numDots := strings.Count(s[lastSlashIdx+1:], ".")
+ numDots := strings.Count(s[lastSlashIdx+1:], ".") - len(skipDots)
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
switch c {
case '.':
+ if isSkippedDot(skipDots, i) {
+ continue
+ }
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
lastDot = i
case '/':
}
}
+ // Compute the name boundary.
+ if p.posNameHigh >= p.posContainerHigh {
+ p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: p.posNameHigh}
+ } else {
+ p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: len(p.s)}
+ }
+
if len(p.identifiersKnown) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
- id := p.identifiersKnown[len(p.identifiersKnown)-1]
- if id.Low > p.posContainerHigh {
- b := p.s[p.posContainerHigh : id.Low-1]
+ if p.posIdentifierName.Low >= p.posContainerHigh && p.posIdentifierName.High > p.posIdentifierName.Low {
+ b := p.s[p.posIdentifierName.Low:p.posIdentifierName.High]
if isContent {
switch b {
case "index":
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
- disabled bool
+
+ // Prefix identifier positions (indices into identifiersKnown).
+ posIdentifierPrefixLanguages []int
+ posIdentifierVersions []int
+ posIdentifierRoles []int
+ posIdentifierPrefixOutputFormat int
+ posIdentifierPrefixKind int
+ posIdentifierPrefixLayout int
+
+ // Name boundary, computed during parse.
+ posIdentifierName types.LowHigh[string]
+ // Position of the dot before the leftmost known identifier.
+ // Set during parseIdentifier, used to compute posIdentifierName.
+ posNameHigh int
+
+ disabled bool
trimLeadingSlash bool
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
+ p.posIdentifierPrefixLanguages = p.posIdentifierPrefixLanguages[:0]
+ p.posIdentifierVersions = p.posIdentifierVersions[:0]
+ p.posIdentifierRoles = p.posIdentifierRoles[:0]
+ p.posIdentifierPrefixOutputFormat = -1
+ p.posIdentifierPrefixKind = -1
+ p.posIdentifierPrefixLayout = -1
+ p.posIdentifierName = types.LowHigh[string]{}
+ p.posNameHigh = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
// Name returns the last element of path without any language identifier.
func (p *Path) NameNoLang() string {
+ if len(p.posIdentifierPrefixLanguages) > 0 {
+ // Prefix language: the wrapper is outside the name boundary, so Name() without
+ // identifiers is the same as stripping all wrappers. Use the name + extension.
+ if len(p.identifiersKnown) > 0 {
+ return p.NameNoIdentifier() + p.s[p.identifiersKnown[0].Low-1:p.identifiersKnown[0].High]
+ }
+ return p.Name()
+ }
i := p.identifierIndex(p.posIdentifierLanguage)
if i == -1 {
return p.Name()
}
-
- return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
+ id := p.identifiersKnown[i]
+ if id.Low-1 < p.posContainerHigh {
+ return p.s[id.High:]
+ }
+ return p.s[p.posContainerHigh:id.Low-1] + p.s[id.High:]
}
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
- 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.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
- }
- }
- return types.LowHigh[string]{
- Low: p.posContainerHigh,
- High: len(p.s),
- }
+ return p.posIdentifierName
}
// Dir returns all but the last element of path, typically the path's directory.
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
+ if len(p.posIdentifierPrefixLanguages) > 0 {
+ return p.base(true, false)
+ }
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
}
func (p *Path) OutputFormat() string {
+ if p.posIdentifierPrefixOutputFormat != -1 {
+ return p.identifierAsString(p.posIdentifierPrefixOutputFormat)
+ }
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
+ if p.posIdentifierPrefixKind != -1 {
+ return p.identifierAsString(p.posIdentifierPrefixKind)
+ }
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
+ if p.posIdentifierPrefixLayout != -1 {
+ return p.identifierAsString(p.posIdentifierPrefixLayout)
+ }
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string {
+ if len(p.posIdentifierPrefixLanguages) > 0 {
+ return p.identifierAsString(p.posIdentifierPrefixLanguages[0])
+ }
return p.identifierAsString(p.posIdentifierLanguage)
}
+func (p *Path) Langs() []string {
+ return p.identifiersAsStrings(p.posIdentifierPrefixLanguages)
+}
+
+func (p *Path) Version() string {
+ if len(p.posIdentifierVersions) > 0 {
+ return p.identifierAsString(p.posIdentifierVersions[0])
+ }
+ return ""
+}
+
+func (p *Path) Versions() []string {
+ return p.identifiersAsStrings(p.posIdentifierVersions)
+}
+
+func (p *Path) Role() string {
+ if len(p.posIdentifierRoles) > 0 {
+ return p.identifierAsString(p.posIdentifierRoles[0])
+ }
+ return ""
+}
+
+func (p *Path) Roles() []string {
+ return p.identifiersAsStrings(p.posIdentifierRoles)
+}
+
+func (p *Path) identifiersAsStrings(positions []int) []string {
+ if len(positions) == 0 {
+ return nil
+ }
+ ids := make([]string, len(positions))
+ for i, pos := range positions {
+ ids[i] = p.identifierAsString(pos)
+ }
+ return ids
+}
+
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCustomWrapper), identifierCustomWrapper)
}
import (
"path/filepath"
+ "reflect"
"testing"
"github.com/gohugoio/hugo/hugofs/files"
)
func newTestParser() *PathParser {
- dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
+ dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3", "v1.0.0"}, []string{"admin", "editor", "viewer", "guest"})
return &PathParser{
LanguageIndex: map[string]int{
c.Assert(p.Custom(), qt.Equals, "myid")
},
},
+ {
+ "Prefix language",
+ "/a/b/p1._language_no_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+ },
+ },
+ {
+ "Prefix version",
+ "/a/b/p1._version_v1_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Version(), qt.Equals, "v1")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "v1"})
+ },
+ },
+ {
+ "Prefix version with dots",
+ "/a/b/p1._version_v1.0.0_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Version(), qt.Equals, "v1.0.0")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Prefix role",
+ "/a/b/p1._role_admin_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Role(), qt.Equals, "admin")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Multiple prefixes",
+ "/a/b/p1._language_no_._role_admin_._version_v2_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Role(), qt.Equals, "admin")
+ c.Assert(p.Version(), qt.Equals, "v2")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Prefix version with dots and role",
+ "/a/b/p1._version_v1.0.0_._role_editor_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Version(), qt.Equals, "v1.0.0")
+ c.Assert(p.Role(), qt.Equals, "editor")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Prefix with dot-based lang",
+ "/a/b/p1._version_v1_.no.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Version(), qt.Equals, "v1")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Prefix leaf bundle with version",
+ "/a/b/index._version_v1_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.IsLeafBundle(), qt.IsTrue)
+ c.Assert(p.Version(), qt.Equals, "v1")
+ c.Assert(p.Base(), qt.Equals, "/a/b")
+ c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
+ },
+ },
+ {
+ "Prefix branch bundle with role",
+ "/a/b/_index._role_admin_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.IsBranchBundle(), qt.IsTrue)
+ c.Assert(p.Role(), qt.Equals, "admin")
+ c.Assert(p.Base(), qt.Equals, "/a/b")
+ },
+ },
+ {
+ "Prefix with mixed case, unnormalized",
+ "/a/b/My Page._Version_V1_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/my-page")
+ c.Assert(p.Version(), qt.Equals, "v1")
+ pp := p.Unnormalized()
+ c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "My Page")
+ c.Assert(pp.Version(), qt.Equals, "V1")
+ },
+ },
+ {
+ "Multiple languages",
+ "/a/b/p1._language_en_._language_fr_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ // Right-to-left parse order.
+ c.Assert(p.Lang(), qt.Equals, "fr")
+ c.Assert(p.Langs(), qt.DeepEquals, []string{"fr", "en"})
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Multiple roles",
+ "/a/b/p1._role_guest_._role_admin_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Role(), qt.Equals, "admin")
+ c.Assert(p.Roles(), qt.DeepEquals, []string{"admin", "guest"})
+ c.Assert(p.Ext(), qt.Equals, "md")
+ },
+ },
+ {
+ "Multiple versions",
+ "/a/b/p1._version_v1_._version_v2_.md",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/a/b/p1")
+ c.Assert(p.Version(), qt.Equals, "v2")
+ c.Assert(p.Versions(), qt.DeepEquals, []string{"v2", "v1"})
+ },
+ },
+ {
+ "Unknown prefix not extracted",
+ "/a/b/p1._unknown_foo_.md",
+ func(c *qt.C, p *Path) {
+ // Unknown prefix should be left in path and treated as custom wrapper.
+ c.Assert(p.Custom(), qt.Equals, "unknown_foo")
+ c.Assert(p.Version(), qt.Equals, "")
+ c.Assert(p.Role(), qt.Equals, "")
+ },
+ },
}
parser := newTestParser()
for _, test := range tests {
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
},
},
+ {
+ "Prefix language layout",
+ "/page._language_no_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "Prefix outputformat layout",
+ "/page._outputformat_amp_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "Prefix kind layout",
+ "/page._kind_section_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "Prefix layout layout",
+ "/page._layout_list_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Layout(), qt.Equals, "list")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "All prefix identifiers in layout",
+ "/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Lang(), qt.Equals, "fr")
+ c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Layout(), qt.Equals, "list")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "Prefix version in layout",
+ "/page._version_v2_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Version(), qt.Equals, "v2")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
+ {
+ "Prefix role in layout",
+ "/page._role_guest_.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Role(), qt.Equals, "guest")
+ c.Assert(p.Base(), qt.Equals, "/page.html")
+ },
+ },
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
- if test.name != "Not lang" {
- return
- }
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
}
+func BenchmarkParseIdentityPrefixes(b *testing.B) {
+ parser := newTestParser()
+ for b.Loop() {
+ parser.ParseIdentity(files.ComponentFolderLayouts, "/a/b._language_en_._version_v1_._role_admin_._layout_list_._kind_page_.html")
+ }
+}
+
func TestSitesMatrixFromPath(t *testing.T) {
c := qt.New(t)
parser := newTestParser()
+
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
v := parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
+
+ // With version prefix.
+ p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md")
+ v = parser.SitesMatrixFromPath(p)
+ c.Assert(v.HasLanguage(2), qt.IsTrue)
+ c.Assert(v.HasVersion(1), qt.IsTrue) // v2 is index 1
+ c.Assert(v.LenVectors(), qt.Equals, 1)
+ c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 1, 0})
+
+ // With version and role prefixes.
+ p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md")
+ v = parser.SitesMatrixFromPath(p)
+ c.Assert(v.HasLanguage(2), qt.IsTrue)
+ c.Assert(v.HasVersion(0), qt.IsTrue) // v1 is index 0
+ c.Assert(v.HasRole(1), qt.IsTrue) // editor is index 1
+ c.Assert(v.LenVectors(), qt.Equals, 1)
+ c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 1})
+
+ // With multiple roles.
+ p = parser.Parse(files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md")
+ v = parser.SitesMatrixFromPath(p)
+ c.Assert(v.HasLanguage(2), qt.IsTrue)
+ c.Assert(v.HasRole(0), qt.IsTrue) // admin is index 0
+ c.Assert(v.HasRole(3), qt.IsTrue) // guest is index 3
+ c.Assert(v.LenVectors(), qt.Equals, 2)
+
+ // With multiple languages via prefix.
+ p = parser.Parse(files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md")
+ v = parser.SitesMatrixFromPath(p)
+ c.Assert(v.HasLanguage(0), qt.IsFalse) // no is index 0, not included
+ c.Assert(v.HasLanguage(1), qt.IsTrue) // en is index 1
+ c.Assert(v.HasLanguage(2), qt.IsTrue) // fr is index 2
+ c.Assert(v.LenVectors(), qt.Equals, 2)
+}
+
+func FuzzParsePath(f *testing.F) {
+ componentPaths := []struct {
+ component string
+ path string
+ }{
+ {files.ComponentFolderContent, "/a/b/c.fr.md"},
+ {files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md"},
+ {files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md"},
+ {files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md"},
+ {files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md"},
+ {files.ComponentFolderLayouts, "/list.no.html"},
+ {files.ComponentFolderLayouts, "/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html"},
+ }
+
+ for _, cp := range componentPaths {
+ f.Add(cp.component, cp.path)
+ }
+
+ parser := newTestParser()
+
+ f.Fuzz(func(t *testing.T, c, s string) {
+ p := parser.Parse(c, s)
+ if p == nil {
+ t.Fatalf("Parse returned nil for path: %q", s)
+ }
+ // Execute all the methods using reflection to ensure they don't panic.
+ v := reflect.ValueOf(p)
+ for i := 0; i < v.NumMethod(); i++ {
+ method := v.Type().Method(i)
+ if method.Type.NumIn() == 1 {
+ method.Func.Call([]reflect.Value{v})
+ }
+ }
+ })
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {