Files
2026-07-13 11:58:46 +08:00

1094 lines
28 KiB
Go

// 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 "<nil>"
}
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")
}