summaryrefslogtreecommitdiffstats
path: root/source
diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/content_directory_test.go2
-rw-r--r--source/fileInfo.go258
-rw-r--r--source/fileInfo_test.go58
-rw-r--r--source/filesystem.go59
-rw-r--r--source/filesystem_test.go80
-rw-r--r--source/sourceSpec.go35
6 files changed, 88 insertions, 404 deletions
diff --git a/source/content_directory_test.go b/source/content_directory_test.go
index 7d1630529..96ee22bc7 100644
--- a/source/content_directory_test.go
+++ b/source/content_directory_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/source/fileInfo.go b/source/fileInfo.go
index 60c6e6ea8..5b24bbeb2 100644
--- a/source/fileInfo.go
+++ b/source/fileInfo.go
@@ -14,9 +14,7 @@
package source
import (
- "fmt"
"path/filepath"
- "strings"
"sync"
"time"
@@ -24,8 +22,6 @@ import (
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/hugofs/files"
-
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/hugofs"
@@ -33,269 +29,121 @@ import (
"github.com/gohugoio/hugo/helpers"
)
-// fileInfo implements the File interface.
-var (
- _ File = (*FileInfo)(nil)
-)
-
-// File represents a source file.
-// This is a temporary construct until we resolve page.Page conflicts.
-// TODO(bep) remove this construct once we have resolved page deprecations
-type File interface {
- fileOverlap
- FileWithoutOverlap
-}
-
-// Temporary to solve duplicate/deprecated names in page.Page
-type fileOverlap interface {
- // Path gets the relative path including file name and extension.
- // The directory is relative to the content root.
- Path() string
-
- // Section is first directory below the content root.
- // For page bundles in root, the Section will be empty.
- Section() string
-
- // Lang is the language code for this page. It will be the
- // same as the site's language code.
- Lang() string
-
- IsZero() bool
-}
-
-type FileWithoutOverlap interface {
- // Filename gets the full path and filename to the file.
- Filename() string
-
- // Dir gets the name of the directory that contains this file.
- // The directory is relative to the content root.
- Dir() string
-
- // Extension is an alias to Ext().
- // Deprecated: Use Ext instead.
- Extension() string
-
- // Ext gets the file extension, i.e "myblogpost.md" will return "md".
- Ext() string
-
- // LogicalName is filename and extension of the file.
- LogicalName() string
-
- // BaseFileName is a filename without extension.
- BaseFileName() string
-
- // TranslationBaseName is a filename with no extension,
- // not even the optional language extension part.
- TranslationBaseName() string
-
- // ContentBaseName is a either TranslationBaseName or name of containing folder
- // if file is a leaf bundle.
- ContentBaseName() string
-
- // UniqueID is the MD5 hash of the file's path and is for most practical applications,
- // Hugo content files being one of them, considered to be unique.
- UniqueID() string
-
- // For internal use only.
- FileInfo() hugofs.FileMetaInfo
-}
-
-// FileInfo describes a source file.
-type FileInfo struct {
- // Absolute filename to the file on disk.
- filename string
-
- sp *SourceSpec
-
- fi hugofs.FileMetaInfo
-
- // Derived from filename
- ext string // Extension without any "."
- lang string
-
- name string
-
- dir string
- relDir string
- relPath string
- baseName string
- translationBaseName string
- contentBaseName string
- section string
- classifier files.ContentClass
+// File describes a source file.
+type File struct {
+ fim hugofs.FileMetaInfo
uniqueID string
-
lazyInit sync.Once
}
// Filename returns a file's absolute path and filename on disk.
-func (fi *FileInfo) Filename() string { return fi.filename }
+func (fi *File) Filename() string { return fi.fim.Meta().Filename }
// Path gets the relative path including file name and extension. The directory
// is relative to the content root.
-func (fi *FileInfo) Path() string { return fi.relPath }
+func (fi *File) Path() string { return filepath.Join(fi.p().Dir()[1:], fi.p().Name()) }
// Dir gets the name of the directory that contains this file. The directory is
// relative to the content root.
-func (fi *FileInfo) Dir() string { return fi.relDir }
+func (fi *File) Dir() string {
+ return fi.pathToDir(fi.p().Dir())
+}
// Extension is an alias to Ext().
-func (fi *FileInfo) Extension() string {
+func (fi *File) Extension() string {
hugo.Deprecate(".File.Extension", "Use .File.Ext instead.", "v0.96.0")
return fi.Ext()
}
-// Ext returns a file's extension without the leading period (ie. "md").
-func (fi *FileInfo) Ext() string { return fi.ext }
+// Ext returns a file's extension without the leading period (e.g. "md").
+// Deprecated: Use Extension() instead.
+func (fi *File) Ext() string { return fi.p().Ext() }
-// Lang returns a file's language (ie. "sv").
-func (fi *FileInfo) Lang() string { return fi.lang }
+// Lang returns a file's language (e.g. "sv").
+func (fi *File) Lang() string {
+ return fi.fim.Meta().Lang
+}
-// LogicalName returns a file's name and extension (ie. "page.sv.md").
-func (fi *FileInfo) LogicalName() string { return fi.name }
+// LogicalName returns a file's name and extension (e.g. "page.sv.md").
+func (fi *File) LogicalName() string {
+ return fi.p().Name()
+}
-// BaseFileName returns a file's name without extension (ie. "page.sv").
-func (fi *FileInfo) BaseFileName() string { return fi.baseName }
+// BaseFileName returns a file's name without extension (e.g. "page.sv").
+func (fi *File) BaseFileName() string {
+ return fi.p().NameNoExt()
+}
// TranslationBaseName returns a file's translation base name without the
-// language segment (ie. "page").
-func (fi *FileInfo) TranslationBaseName() string { return fi.translationBaseName }
+// language segment (e.g. "page").
+func (fi *File) TranslationBaseName() string { return fi.p().NameNoIdentifier() }
// ContentBaseName is a either TranslationBaseName or name of containing folder
-// if file is a leaf bundle.
-func (fi *FileInfo) ContentBaseName() string {
- fi.init()
- return fi.contentBaseName
+// if file is a bundle.
+func (fi *File) ContentBaseName() string {
+ return fi.p().BaseNameNoIdentifier()
}
// Section returns a file's section.
-func (fi *FileInfo) Section() string {
- fi.init()
- return fi.section
+func (fi *File) Section() string {
+ return fi.p().Section()
}
// UniqueID returns a file's unique, MD5 hash identifier.
-func (fi *FileInfo) UniqueID() string {
+func (fi *File) UniqueID() string {
fi.init()
return fi.uniqueID
}
// FileInfo returns a file's underlying os.FileInfo.
-// For internal use only.
-func (fi *FileInfo) FileInfo() hugofs.FileMetaInfo { return fi.fi }
+func (fi *File) FileInfo() hugofs.FileMetaInfo { return fi.fim }
-func (fi *FileInfo) String() string { return fi.BaseFileName() }
+func (fi *File) String() string { return fi.BaseFileName() }
// Open implements ReadableFile.
-func (fi *FileInfo) Open() (hugio.ReadSeekCloser, error) {
- f, err := fi.fi.Meta().Open()
+func (fi *File) Open() (hugio.ReadSeekCloser, error) {
+ f, err := fi.fim.Meta().Open()
return f, err
}
-func (fi *FileInfo) IsZero() bool {
+func (fi *File) IsZero() bool {
return fi == nil
}
// We create a lot of these FileInfo objects, but there are parts of it used only
// in some cases that is slightly expensive to construct.
-func (fi *FileInfo) init() {
+func (fi *File) init() {
fi.lazyInit.Do(func() {
- relDir := strings.Trim(fi.relDir, helpers.FilePathSeparator)
- parts := strings.Split(relDir, helpers.FilePathSeparator)
- var section string
- if (fi.classifier != files.ContentClassLeaf && len(parts) == 1) || len(parts) > 1 {
- section = parts[0]
- }
- fi.section = section
-
- if fi.classifier.IsBundle() && len(parts) > 0 {
- fi.contentBaseName = parts[len(parts)-1]
- } else {
- fi.contentBaseName = fi.translationBaseName
- }
-
- fi.uniqueID = helpers.MD5String(filepath.ToSlash(fi.relPath))
+ fi.uniqueID = helpers.MD5String(filepath.ToSlash(fi.Path()))
})
}
-// NewTestFile creates a partially filled File used in unit tests.
-// TODO(bep) improve this package
-func NewTestFile(filename string) *FileInfo {
- base := filepath.Base(filepath.Dir(filename))
- return &FileInfo{
- filename: filename,
- translationBaseName: base,
+func (fi *File) pathToDir(s string) string {
+ if s == "" {
+ return s
}
+ return filepath.FromSlash(s[1:] + "/")
}
-func (sp *SourceSpec) NewFileInfoFrom(path, filename string) (*FileInfo, error) {
+func (fi *File) p() *paths.Path {
+ return fi.fim.Meta().PathInfo
+}
+
+func NewFileInfoFrom(path, filename string) *File {
meta := &hugofs.FileMeta{
Filename: filename,
- Path: path,
+ PathInfo: paths.Parse("", filepath.ToSlash(path)),
}
- return sp.NewFileInfo(hugofs.NewFileMetaInfo(nil, meta))
+ return NewFileInfo(hugofs.NewFileMetaInfo(nil, meta))
}
-func (sp *SourceSpec) NewFileInfo(fi hugofs.FileMetaInfo) (*FileInfo, error) {
- m := fi.Meta()
-
- filename := m.Filename
- relPath := m.Path
-
- if relPath == "" {
- return nil, fmt.Errorf("no Path provided by %v (%T)", m, m.Fs)
- }
-
- if filename == "" {
- return nil, fmt.Errorf("no Filename provided by %v (%T)", m, m.Fs)
+func NewFileInfo(fi hugofs.FileMetaInfo) *File {
+ return &File{
+ fim: fi,
}
-
- relDir := filepath.Dir(relPath)
- if relDir == "." {
- relDir = ""
- }
- if !strings.HasSuffix(relDir, helpers.FilePathSeparator) {
- relDir = relDir + helpers.FilePathSeparator
- }
-
- lang := m.Lang
- translationBaseName := m.TranslationBaseName
-
- dir, name := filepath.Split(relPath)
- if !strings.HasSuffix(dir, helpers.FilePathSeparator) {
- dir = dir + helpers.FilePathSeparator
- }
-
- ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), "."))
- baseName := paths.Filename(name)
-
- if translationBaseName == "" {
- // This is usually provided by the filesystem. But this FileInfo is also
- // created in a standalone context when doing "hugo new". This is
- // an approximate implementation, which is "good enough" in that case.
- fileLangExt := filepath.Ext(baseName)
- translationBaseName = strings.TrimSuffix(baseName, fileLangExt)
- }
-
- f := &FileInfo{
- sp: sp,
- filename: filename,
- fi: fi,
- lang: lang,
- ext: ext,
- dir: dir,
- relDir: relDir, // Dir()
- relPath: relPath, // Path()
- name: name,
- baseName: baseName, // BaseFileName()
- translationBaseName: translationBaseName,
- classifier: m.Classifier,
- }
-
- return f, nil
}
func NewGitInfo(info gitmap.GitInfo) GitInfo {
diff --git a/source/fileInfo_test.go b/source/fileInfo_test.go
deleted file mode 100644
index e2a3edd30..000000000
--- a/source/fileInfo_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2017-present 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 source_test
-
-import (
- "path/filepath"
- "strings"
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/source"
-)
-
-func TestFileInfo(t *testing.T) {
- c := qt.New(t)
-
- s := newTestSourceSpec()
-
- for _, this := range []struct {
- base string
- filename string
- assert func(f *source.FileInfo)
- }{
- {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/page.md"), func(f *source.FileInfo) {
- c.Assert(f.Filename(), qt.Equals, filepath.FromSlash("/a/b/page.md"))
- c.Assert(f.Dir(), qt.Equals, filepath.FromSlash("b/"))
- c.Assert(f.Path(), qt.Equals, filepath.FromSlash("b/page.md"))
- c.Assert(f.Section(), qt.Equals, "b")
- c.Assert(f.TranslationBaseName(), qt.Equals, filepath.FromSlash("page"))
- c.Assert(f.BaseFileName(), qt.Equals, filepath.FromSlash("page"))
- }},
- {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/c/d/page.md"), func(f *source.FileInfo) {
- c.Assert(f.Section(), qt.Equals, "b")
- }},
- {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/page.en.MD"), func(f *source.FileInfo) {
- c.Assert(f.Section(), qt.Equals, "b")
- c.Assert(f.Path(), qt.Equals, filepath.FromSlash("b/page.en.MD"))
- c.Assert(f.TranslationBaseName(), qt.Equals, filepath.FromSlash("page"))
- c.Assert(f.BaseFileName(), qt.Equals, filepath.FromSlash("page.en"))
- }},
- } {
- path := strings.TrimPrefix(this.filename, this.base)
- f, err := s.NewFileInfoFrom(path, this.filename)
- c.Assert(err, qt.IsNil)
- this.assert(f)
- }
-}
diff --git a/source/filesystem.go b/source/filesystem.go
index 283863dbf..208f5036f 100644
--- a/source/filesystem.go
+++ b/source/filesystem.go
@@ -14,66 +14,27 @@
package source
import (
- "fmt"
"path/filepath"
- "sync"
"github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/afero"
)
// Filesystem represents a source filesystem.
type Filesystem struct {
- files []File
- filesInit sync.Once
- filesInitErr error
-
Base string
-
- fi hugofs.FileMetaInfo
-
+ fs afero.Fs
+ fi hugofs.FileMetaInfo
SourceSpec
}
-// NewFilesystem returns a new filesystem for a given source spec.
+// NewFilesystem returns a new filesytem for a given source spec.
func (sp SourceSpec) NewFilesystem(base string) *Filesystem {
- return &Filesystem{SourceSpec: sp, Base: base}
+ return &Filesystem{SourceSpec: sp, Base: base, fs: sp.Fs.Source}
}
-func (sp SourceSpec) NewFilesystemFromFileMetaInfo(fi hugofs.FileMetaInfo) *Filesystem {
- return &Filesystem{SourceSpec: sp, fi: fi}
-}
-
-// Files returns a slice of readable files.
-func (f *Filesystem) Files() ([]File, error) {
- f.filesInit.Do(func() {
- err := f.captureFiles()
- if err != nil {
- f.filesInitErr = fmt.Errorf("capture files: %w", err)
- }
- })
- return f.files, f.filesInitErr
-}
-
-// add populates a file in the Filesystem.files
-func (f *Filesystem) add(name string, fi hugofs.FileMetaInfo) (err error) {
- var file File
-
- file, err = f.SourceSpec.NewFileInfo(fi)
- if err != nil {
- return err
- }
-
- f.files = append(f.files, file)
-
- return err
-}
-
-func (f *Filesystem) captureFiles() error {
- walker := func(path string, fi hugofs.FileMetaInfo, err error) error {
- if err != nil {
- return err
- }
-
+func (f *Filesystem) Walk(addFile func(*File) error) error {
+ walker := func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
}
@@ -87,14 +48,16 @@ func (f *Filesystem) captureFiles() error {
}
if b {
- err = f.add(filename, fi)
+ if err = addFile(NewFileInfo(fi)); err != nil {
+ return err
+ }
}
return err
}
w := hugofs.NewWalkway(hugofs.WalkwayConfig{
- Fs: f.SourceFs,
+ Fs: f.fs,
Info: f.fi,
Root: f.Base,
WalkFn: walker,
diff --git a/source/filesystem_test.go b/source/filesystem_test.go
index 1067d5839..9118285da 100644
--- a/source/filesystem_test.go
+++ b/source/filesystem_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -14,71 +14,35 @@
package source_test
import (
- "fmt"
- "path/filepath"
"runtime"
"testing"
- "github.com/spf13/afero"
-
qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/helpers"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/source"
+ "github.com/gohugoio/hugo/hugolib"
+ "golang.org/x/text/unicode/norm"
)
-func TestEmptySourceFilesystem(t *testing.T) {
- c := qt.New(t)
- ss := newTestSourceSpec()
- src := ss.NewFilesystem("")
- files, err := src.Files()
- c.Assert(err, qt.IsNil)
- if len(files) != 0 {
- t.Errorf("new filesystem should contain 0 files.")
- }
-}
-
func TestUnicodeNorm(t *testing.T) {
if runtime.GOOS != "darwin" {
- // Normalization code is only for Mac OS, since it is not necessary for other OSes.
- return
- }
-
- c := qt.New(t)
-
- paths := []struct {
- NFC string
- NFD string
- }{
- {NFC: "å", NFD: "\x61\xcc\x8a"},
- {NFC: "é", NFD: "\x65\xcc\x81"},
- }
-
- ss := newTestSourceSpec()
-
- for i, path := range paths {
- base := fmt.Sprintf("base%d", i)
- c.Assert(afero.WriteFile(ss.Fs.Source, filepath.Join(base, path.NFD), []byte("some data"), 0777), qt.IsNil)
- src := ss.NewFilesystem(base)
- files, err := src.Files()
- c.Assert(err, qt.IsNil)
- f := files[0]
- if f.BaseFileName() != path.NFC {
- t.Fatalf("file %q name in NFD form should be normalized (%s)", f.BaseFileName(), path.NFC)
- }
+ t.Skip("Skipping test on non-Darwin OS")
}
-}
-
-func newTestSourceSpec() *source.SourceSpec {
- v := config.New()
- afs := hugofs.NewBaseFileDecorator(afero.NewMemMapFs())
- conf := testconfig.GetTestConfig(afs, v)
- fs := hugofs.NewFrom(afs, conf.BaseConfig())
- ps, err := helpers.NewPathSpec(fs, conf, nil)
- if err != nil {
- panic(err)
+ t.Parallel()
+ files := `
+-- hugo.toml --
+-- content/å.md --
+-- content/é.md --
+-- content/å/å.md --
+-- content/é/é.md --
+-- layouts/_default/single.html --
+Title: {{ .Title }}|File: {{ .File.Path}}
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWithNFDOnDarwin())
+
+ for _, p := range b.H.Sites[0].RegularPages() {
+ f := p.File()
+ b.Assert(norm.NFC.IsNormalString(f.Path()), qt.IsTrue)
+ b.Assert(norm.NFC.IsNormalString(f.Dir()), qt.IsTrue)
+ b.Assert(norm.NFC.IsNormalString(f.Filename()), qt.IsTrue)
+ b.Assert(norm.NFC.IsNormalString(f.BaseFileName()), qt.IsTrue)
}
- return source.NewSourceSpec(ps, nil, fs.Source)
}
diff --git a/source/sourceSpec.go b/source/sourceSpec.go
index dc44994a8..ea1b977f3 100644
--- a/source/sourceSpec.go
+++ b/source/sourceSpec.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -15,7 +15,6 @@
package source
import (
- "os"
"path/filepath"
"runtime"
@@ -38,7 +37,6 @@ type SourceSpec struct {
// NewSourceSpec initializes SourceSpec using languages the given filesystem and PathSpec.
func NewSourceSpec(ps *helpers.PathSpec, inclusionFilter *glob.FilenameFilter, fs afero.Fs) *SourceSpec {
-
shouldInclude := func(filename string) bool {
if !inclusionFilter.Match(filename, false) {
return false
@@ -90,34 +88,3 @@ func (s *SourceSpec) IgnoreFile(filename string) bool {
return false
}
-
-// IsRegularSourceFile returns whether filename represents a regular file in the
-// source filesystem.
-func (s *SourceSpec) IsRegularSourceFile(filename string) (bool, error) {
- fi, err := helpers.LstatIfPossible(s.SourceFs, filename)
- if err != nil {
- return false, err
- }
-
- if fi.IsDir() {
- return false, nil
- }
-
- if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
- link, err := filepath.EvalSymlinks(filename)
- if err != nil {
- return false, err
- }
-
- fi, err = helpers.LstatIfPossible(s.SourceFs, link)
- if err != nil {
- return false, err
- }
-
- if fi.IsDir() {
- return false, nil
- }
- }
-
- return true, nil
-}