// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package paths import ( "fmt" "path" "path/filepath" "runtime" "strings" "sync" "github.com/gohugoio/hugo/common/hmaps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugolib/sitesmatrix" "github.com/gohugoio/hugo/resources/kinds" ) const ( identifierBaseof = "baseof" 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 { return len(s) > 2*len(identifierCustomWrapper) && strings.HasPrefix(s, identifierCustomWrapper) && 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. LanguageIndex map[string]int // Reports whether the given language is disabled. IsLangDisabled func(string) bool // IsOutputFormat reports whether the given name is a valid output format. // The second argument is optional. IsOutputFormat func(name, ext string) bool // Reports whether the given ext is a content file. IsContentExt func(string) bool // The configured sites matrix. ConfiguredDimensions *sitesmatrix.ConfiguredDimensions // Below gets created on demand. initOnce sync.Once sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store. } func (pp *PathParser) init() { pp.initOnce.Do(func() { pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]() }) } // NormalizePathString returns a normalized path string using the very basic Hugo rules. func NormalizePathStringBasic(s string) string { // All lower case. s = strings.ToLower(s) // Replace spaces with hyphens. s = strings.ReplaceAll(s, " ", "-") return s } func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore { pp.init() 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 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: builder.WithDefaultsIfNotSet() case files.ComponentFolderLayouts: builder.WithAllIfNotSet() case files.ComponentFolderStatic: builder.WithDefaultsAndAllLanguagesIfNotSet() } return builder.Build(), nil }) return v } // ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier. func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) { p := pp.parsePooled(c, s) defer putPath(p) return p.Base(), p.BaseNameNoIdentifier() } func (pp *PathParser) parsePooled(c, s string) *Path { s = NormalizePathStringBasic(s) p := getPath() p.component = c p, err := pp.doParse(c, s, p) if err != nil { panic(err) } return p } // Parse parses component c with path s into Path using Hugo's content path rules. func (pp *PathParser) Parse(c, s string) *Path { p, err := pp.parse(c, s) if err != nil { panic(err) } return p } func (pp *PathParser) newPath(component string) *Path { p := &Path{} p.reset() p.component = component return p } func (pp *PathParser) parse(component, s string) (*Path, error) { ss := NormalizePathStringBasic(s) p, err := pp.doParse(component, ss, pp.newPath(component)) if err != nil { return nil, err } if s != ss { var err error // Preserve the original case for titles etc. p.unnormalized, err = pp.doParse(component, s, pp.newPath(component)) if err != nil { return nil, err } } else { p.unnormalized = p } return p, nil } func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) { if p.posContainerHigh != -1 { return } mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) mayHaveOutputFormat := component == files.ComponentFolderLayouts mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat var mayHaveLayout bool if p.pathType == TypeShortcode { mayHaveLayout = !isLast && component == files.ComponentFolderLayouts } else { mayHaveLayout = component == files.ComponentFolderLayouts } var found bool var high int if len(p.identifiersKnown) > 0 { high = lastDot } else { high = len(p.s) } id := types.LowHigh[string]{Low: i + 1, High: high} sid := p.s[id.Low:id.High] if isCustomWrapperIdentifier(sid) { 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 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 // May also be the output format. if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") { p.posIdentifierOutputFormat = 0 } } else { var langFound bool if mayHaveLang { var disabled bool _, langFound = pp.LanguageIndex[sid] if !langFound { disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid) if disabled { p.disabled = true langFound = true } } found = langFound if langFound { p.identifiersKnown = append(p.identifiersKnown, id) p.posIdentifierLanguage = len(p.identifiersKnown) - 1 } } if !found && mayHaveOutputFormat { // At this point we may already have resolved an output format, // but we need to keep looking for a more specific one, e.g. amp before html. // Use both name and extension to prevent // false positives on the form css.html. if pp.IsOutputFormat(sid, p.Ext()) { found = true p.identifiersKnown = append(p.identifiersKnown, id) p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1 } } if !found && mayHaveKind { if kinds.GetKindMain(sid) != "" { found = true p.identifiersKnown = append(p.identifiersKnown, id) p.posIdentifierKind = len(p.identifiersKnown) - 1 } } if !found && sid == identifierBaseof { found = true p.identifiersKnown = append(p.identifiersKnown, id) p.posIdentifierBaseof = len(p.identifiersKnown) - 1 } if !found && mayHaveLayout { if p.posIdentifierLayout != -1 { // Move it to identifiersUnknown. p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout]) p.identifiersKnown[p.posIdentifierLayout] = id } else { p.identifiersKnown = append(p.identifiersKnown, id) p.posIdentifierLayout = len(p.identifiersKnown) - 1 } found = true } if !found { p.identifiersUnknown = append(p.identifiersUnknown, id) } } 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) { if runtime.GOOS == "windows" { s = path.Clean(filepath.ToSlash(s)) if s == "." { s = "" } } if s == "" { s = "/" } // Leading slash, no trailing slash. if !strings.HasPrefix(s, "/") { s = "/" + s } if s != "/" && s[len(s)-1] == '/' { s = s[:len(s)-1] } 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:], ".") - len(skipDots) if strings.Contains(s, "/_shortcodes/") { p.pathType = TypeShortcode } for i := len(s) - 1; i >= 0; i-- { c := s[i] switch c { case '.': if isSkippedDot(skipDots, i) { continue } pp.parseIdentifier(component, s, p, i, lastDot, numDots, false) lastDot = i case '/': slashCount++ if p.posContainerHigh == -1 { if lastDot > 0 { pp.parseIdentifier(component, s, p, i, lastDot, numDots, true) } p.posContainerHigh = i + 1 } else if p.posContainerLow == -1 { p.posContainerLow = i + 1 } if i > 0 { p.posSectionHigh = i } } } // 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()) 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": p.pathType = TypeLeaf case "_index": p.pathType = TypeBranch default: p.pathType = TypeContentSingle } if slashCount == 2 && p.IsLeafBundle() { p.posSectionHigh = 0 } } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) { p.pathType = TypeContentData } } } if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts { if p.posIdentifierBaseof != -1 { p.pathType = TypeBaseof } else { pth := p.Path() if strings.Contains(pth, "/_shortcodes/") { p.pathType = TypeShortcode } else if strings.Contains(pth, "/_markup/") { p.pathType = TypeMarkup } else if strings.HasPrefix(pth, "/_partials/") { p.pathType = TypePartial } } } if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 { id := p.identifiersKnown[p.posIdentifierLayout] if id.Low == p.posContainerHigh { // First identifier is shortcode name. p.posIdentifierLayout = -1 } } return p, nil } func ModifyPathBundleTypeResource(p *Path) { if p.IsContent() { p.pathType = TypeContentResource } else { p.pathType = TypeFile } } //go:generate stringer -type Type type Type int const ( // A generic file, e.g. a JSON file. TypeFile Type = iota // All below are content files. // A resource of a content type with front matter. TypeContentResource // E.g. /blog/my-post.md TypeContentSingle // All below are bundled content files. // Leaf bundles, e.g. /blog/my-post/index.md TypeLeaf // Branch bundles, e.g. /blog/_index.md TypeBranch // Content data file, _content.gotmpl. TypeContentData // Layout types. TypeMarkup TypeShortcode TypePartial TypeBaseof ) type Path struct { // Note: Any additions to this struct should also be added to the pathPool. s string posContainerLow int posContainerHigh int posSectionHigh int component string pathType Type identifiersKnown []types.LowHigh[string] identifiersUnknown []types.LowHigh[string] posIdentifierLanguage int posIdentifierOutputFormat int posIdentifierKind int posIdentifierLayout int posIdentifierBaseof int posIdentifierCustom int // 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 unnormalized *Path } var pathPool = &sync.Pool{ New: func() any { p := &Path{} p.reset() return p }, } func getPath() *Path { return pathPool.Get().(*Path) } func putPath(p *Path) { p.reset() pathPool.Put(p) } func (p *Path) reset() { p.s = "" p.posContainerLow = -1 p.posContainerHigh = -1 p.posSectionHigh = -1 p.component = "" p.pathType = 0 p.identifiersKnown = p.identifiersKnown[:0] p.posIdentifierLanguage = -1 p.posIdentifierOutputFormat = -1 p.posIdentifierKind = -1 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 } // TrimLeadingSlash returns a copy of the Path with the leading slash removed. func (p Path) TrimLeadingSlash() *Path { p.trimLeadingSlash = true return &p } func (p *Path) norm(s string) string { if p.trimLeadingSlash { s = strings.TrimPrefix(s, "/") } return s } // IdentifierBase satisfies identity.Identity. func (p *Path) IdentifierBase() string { if p.Component() == files.ComponentFolderLayouts { return p.Path() } return p.Base() } // Component returns the component for this path (e.g. "content"). func (p *Path) Component() string { return p.component } // Container returns the base name of the container directory for this path. func (p *Path) Container() string { if p.posContainerLow == -1 { return "" } return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1]) } func (p *Path) String() string { if p == nil { return "" } return p.Path() } // ContainerDir returns the container directory for this path. // For content bundles this will be the parent directory. func (p *Path) ContainerDir() string { if p.posContainerLow == -1 || !p.IsBundle() { return p.Dir() } return p.norm(p.s[:p.posContainerLow-1]) } // Section returns the first path element (section). func (p *Path) Section() string { if p.posSectionHigh <= 0 { return "" } return p.norm(p.s[1:p.posSectionHigh]) } // IsContent returns true if the path is a content file (e.g. mypost.md). // Note that this will also return true for content files in a bundle. func (p *Path) IsContent() bool { return p.Type() >= TypeContentResource && p.Type() <= TypeContentData } // isContentPage returns true if the path is a content file (e.g. mypost.md), // but nof if inside a leaf bundle. func (p *Path) isContentPage() bool { return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData } // Name returns the last element of path. func (p *Path) Name() string { if p.posContainerHigh > 0 { return p.s[p.posContainerHigh:] } return p.s } // 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.identifiersKnown[i].Low-1] } return p.s[p.posContainerHigh:] } // BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension). // For bundles this will be the containing directory's name, e.g. "blog". func (p *Path) BaseNameNoIdentifier() string { if p.IsBundle() { return p.Container() } return p.NameNoIdentifier() } // NameNoIdentifier returns the last element of path without any identifier (e.g. no extension). func (p *Path) NameNoIdentifier() string { lowHigh := p.nameLowHigh() return p.s[lowHigh.Low:lowHigh.High] } func (p *Path) nameLowHigh() types.LowHigh[string] { return p.posIdentifierName } // Dir returns all but the last element of path, typically the path's directory. func (p *Path) Dir() (d string) { if p.posContainerHigh > 0 { d = p.s[:p.posContainerHigh-1] } if d == "" { d = "/" } d = p.norm(d) return } // Path returns the full path. func (p *Path) Path() (d string) { return p.norm(p.s) } // PathNoLeadingSlash returns the full path without the leading slash. func (p *Path) PathNoLeadingSlash() string { return p.Path()[1:] } // Unnormalized returns the Path with the original case preserved. func (p *Path) Unnormalized() *Path { return p.unnormalized } // 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() } return p.base(true, false) } // PathNoIdentifier returns the Path but with any identifier (ext, lang) removed. func (p *Path) PathNoIdentifier() string { return p.base(false, false) } // 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.identifiersKnown) == 0 { return p.norm(p.s) } i := p.identifierIndex(0) if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) { i = j } if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) { i = j } if i == -1 { return p.norm(p.s) } id := p.identifiersKnown[i] return p.norm(p.s[:id.Low-1]) } // PathRel returns the path relative to the given owner. func (p *Path) PathRel(owner *Path) string { ob := owner.Base() if !strings.HasSuffix(ob, "/") { ob += "/" } return strings.TrimPrefix(p.Path(), ob) } // BaseRel returns the base path relative to the given owner. func (p *Path) BaseRel(owner *Path) string { ob := owner.Base() if ob == "/" { ob = "" } return p.Base()[len(ob)+1:] } // For content files, Base returns the path without any identifiers (extension, language code etc.). // Any 'index' as the last path element is ignored. // // For other files (Resources), any extension is kept. func (p *Path) Base() string { s := p.base(!p.isContentPage(), p.IsBundle()) if s == "/" && p.isContentPage() { // The content home page is represented as "". s = "" } return s } // Used in template lookups. // For pages with Type set, we treat that as the section. func (p *Path) BaseReTyped(typ string) (d string) { base := p.Base() if typ == "" || p.Section() == typ { return base } d = "/" + typ if p.posSectionHigh != -1 { d += base[p.posSectionHigh:] } d = p.norm(d) return } func (p *Path) base(preserveExt, isBundle bool) string { if len(p.identifiersKnown) == 0 { return p.norm(p.s) } if preserveExt && len(p.identifiersKnown) == 1 { // Preserve extension. return p.norm(p.s) } var high int if isBundle { high = p.posContainerHigh - 1 } else { high = p.nameLowHigh().High } if high == 0 { high++ } if !preserveExt { return p.norm(p.s[:high]) } // For txt files etc. we want to preserve the extension. id := p.identifiersKnown[0] return p.norm(p.s[:high] + p.s[id.Low-1:id.High]) } func (p *Path) Ext() string { return p.identifierAsString(0) } 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) Versions() []string { return p.identifiersAsStrings(p.posIdentifierVersions) } 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) } func (p *Path) Disabled() bool { return p.disabled } func (p *Path) Identifiers() []string { ids := make([]string, len(p.identifiersKnown)) for i, id := range p.identifiersKnown { ids[i] = p.s[id.Low:id.High] } return ids } func (p *Path) IdentifiersUnknown() []string { ids := make([]string, len(p.identifiersUnknown)) for i, id := range p.identifiersUnknown { ids[i] = p.s[id.Low:id.High] } return ids } func (p *Path) Type() Type { return p.pathType } func (p *Path) IsBundle() bool { return p.pathType >= TypeLeaf && p.pathType <= TypeContentData } func (p *Path) IsBranchBundle() bool { return p.pathType == TypeBranch } func (p *Path) IsLeafBundle() bool { return p.pathType == TypeLeaf } func (p *Path) IsContentData() bool { return p.pathType == TypeContentData } func (p Path) ForType(t Type) *Path { p.pathType = t return &p } func (p *Path) identifierAsString(i int) string { i = p.identifierIndex(i) if i == -1 { return "" } id := p.identifiersKnown[i] return p.s[id.Low:id.High] } func (p *Path) identifierIndex(i int) int { if i < 0 || i >= len(p.identifiersKnown) { return -1 } return i } // HasExt returns true if the Unix styled path has an extension. func HasExt(p string) bool { for i := len(p) - 1; i >= 0; i-- { if p[i] == '.' { return true } if p[i] == '/' { return false } } return false } // ValidateIdentifier returns true if the given string is a valid identifier according // to Hugo's basic path normalization rules. func ValidateIdentifier(s string) error { if s == NormalizePathStringBasic(s) { return nil } return fmt.Errorf("must be all lower case and no spaces") }