diff options
Diffstat (limited to 'hugofs')
-rw-r--r-- | hugofs/basepath_real_filename_fs.go | 91 | ||||
-rw-r--r-- | hugofs/decorators.go | 205 | ||||
-rw-r--r-- | hugofs/fileinfo.go | 297 | ||||
-rw-r--r-- | hugofs/files/classifier.go | 121 | ||||
-rw-r--r-- | hugofs/files/classifier_test.go | 49 | ||||
-rw-r--r-- | hugofs/filter_fs.go | 341 | ||||
-rw-r--r-- | hugofs/filter_fs_test.go | 48 | ||||
-rw-r--r-- | hugofs/fs.go | 6 | ||||
-rw-r--r-- | hugofs/language_composite_fs.go | 40 | ||||
-rw-r--r-- | hugofs/language_composite_fs_test.go | 107 | ||||
-rw-r--r-- | hugofs/language_fs.go | 346 | ||||
-rw-r--r-- | hugofs/language_fs_test.go | 100 | ||||
-rw-r--r-- | hugofs/nolstat_fs.go | 39 | ||||
-rw-r--r-- | hugofs/nosymlink_fs.go | 85 | ||||
-rw-r--r-- | hugofs/nosymlink_test.go | 97 | ||||
-rw-r--r-- | hugofs/rootmapping_fs.go | 457 | ||||
-rw-r--r-- | hugofs/rootmapping_fs_test.go | 199 | ||||
-rw-r--r-- | hugofs/slice_fs.go | 293 | ||||
-rw-r--r-- | hugofs/walk.go | 308 | ||||
-rw-r--r-- | hugofs/walk_test.go | 225 |
20 files changed, 2663 insertions, 791 deletions
diff --git a/hugofs/basepath_real_filename_fs.go b/hugofs/basepath_real_filename_fs.go deleted file mode 100644 index 1024c4d30..000000000 --- a/hugofs/basepath_real_filename_fs.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2018 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 hugofs - -import ( - "os" - - "github.com/spf13/afero" -) - -// RealFilenameInfo is a thin wrapper around os.FileInfo adding the real filename. -type RealFilenameInfo interface { - os.FileInfo - - // This is the real filename to the file in the underlying filesystem. - RealFilename() string -} - -type realFilenameInfo struct { - os.FileInfo - realFilename string -} - -func (f *realFilenameInfo) RealFilename() string { - return f.realFilename -} - -// NewBasePathRealFilenameFs returns a new BasePathRealFilenameFs instance -// using base. -func NewBasePathRealFilenameFs(base *afero.BasePathFs) *BasePathRealFilenameFs { - return &BasePathRealFilenameFs{BasePathFs: base} -} - -// BasePathRealFilenameFs is a thin wrapper around afero.BasePathFs that -// provides the real filename in Stat and LstatIfPossible. -type BasePathRealFilenameFs struct { - *afero.BasePathFs -} - -// Stat returns the os.FileInfo structure describing a given file. If there is -// an error, it will be of type *os.PathError. -func (b *BasePathRealFilenameFs) Stat(name string) (os.FileInfo, error) { - fi, err := b.BasePathFs.Stat(name) - if err != nil { - return nil, err - } - - if _, ok := fi.(RealFilenameInfo); ok { - return fi, nil - } - - filename, err := b.RealPath(name) - if err != nil { - return nil, &os.PathError{Op: "stat", Path: name, Err: err} - } - - return &realFilenameInfo{FileInfo: fi, realFilename: filename}, nil -} - -// LstatIfPossible returns the os.FileInfo structure describing a given file. -// It attempts to use Lstat if supported or defers to the os. In addition to -// the FileInfo, a boolean is returned telling whether Lstat was called. -func (b *BasePathRealFilenameFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { - - fi, ok, err := b.BasePathFs.LstatIfPossible(name) - if err != nil { - return nil, false, err - } - - if _, ok := fi.(RealFilenameInfo); ok { - return fi, ok, nil - } - - filename, err := b.RealPath(name) - if err != nil { - return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err} - } - - return &realFilenameInfo{FileInfo: fi, realFilename: filename}, ok, nil -} diff --git a/hugofs/decorators.go b/hugofs/decorators.go new file mode 100644 index 000000000..0a2b39712 --- /dev/null +++ b/hugofs/decorators.go @@ -0,0 +1,205 @@ +// Copyright 2019 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 hugofs + +import ( + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/spf13/afero" +) + +func decorateDirs(fs afero.Fs, meta FileMeta) afero.Fs { + ffs := &baseFileDecoratorFs{Fs: fs} + + decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) { + if !fi.IsDir() { + // Leave regular files as they are. + return fi, nil + } + + return decorateFileInfo(fi, fs, nil, "", "", meta), nil + } + + ffs.decorate = decorator + + return ffs + +} + +func decoratePath(fs afero.Fs, createPath func(name string) string) afero.Fs { + + ffs := &baseFileDecoratorFs{Fs: fs} + + decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) { + path := createPath(name) + + return decorateFileInfo(fi, fs, nil, "", path, nil), nil + } + + ffs.decorate = decorator + + return ffs + +} + +// DecorateBasePathFs adds Path info to files and directories in the +// provided BasePathFs, using the base as base. +func DecorateBasePathFs(base *afero.BasePathFs) afero.Fs { + basePath, _ := base.RealPath("") + if !strings.HasSuffix(basePath, filepathSeparator) { + basePath += filepathSeparator + } + + ffs := &baseFileDecoratorFs{Fs: base} + + decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) { + path := strings.TrimPrefix(name, basePath) + + return decorateFileInfo(fi, base, nil, "", path, nil), nil + } + + ffs.decorate = decorator + + return ffs +} + +// NewBaseFileDecorator decorates the given Fs to provide the real filename +// and an Opener func. If +func NewBaseFileDecorator(fs afero.Fs) afero.Fs { + + ffs := &baseFileDecoratorFs{Fs: fs} + + decorator := func(fi os.FileInfo, filename string) (os.FileInfo, error) { + // Store away the original in case it's a symlink. + meta := FileMeta{metaKeyName: fi.Name()} + isSymlink := isSymlink(fi) + if isSymlink { + meta[metaKeyOriginalFilename] = filename + link, err := filepath.EvalSymlinks(filename) + if err != nil { + return nil, err + } + + fi, err = fs.Stat(link) + if err != nil { + return nil, err + } + + filename = link + meta[metaKeyIsSymlink] = true + + } + + opener := func() (afero.File, error) { + return ffs.open(filename) + + } + + return decorateFileInfo(fi, ffs, opener, filename, "", meta), nil + } + + ffs.decorate = decorator + return ffs +} + +type baseFileDecoratorFs struct { + afero.Fs + decorate func(fi os.FileInfo, filename string) (os.FileInfo, error) +} + +func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) { + fi, err := fs.Fs.Stat(name) + if err != nil { + return nil, err + } + + return fs.decorate(fi, name) + +} + +func (fs *baseFileDecoratorFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + var ( + fi os.FileInfo + err error + ok bool + ) + + if lstater, isLstater := fs.Fs.(afero.Lstater); isLstater { + fi, ok, err = lstater.LstatIfPossible(name) + } else { + fi, err = fs.Fs.Stat(name) + } + + if err != nil { + return nil, false, err + } + + fi, err = fs.decorate(fi, name) + + return fi, ok, err +} + +func (fs *baseFileDecoratorFs) Open(name string) (afero.File, error) { + return fs.open(name) +} + +func (fs *baseFileDecoratorFs) open(name string) (afero.File, error) { + f, err := fs.Fs.Open(name) + if err != nil { + return nil, err + } + return &baseFileDecoratorFile{File: f, fs: fs}, nil +} + +type baseFileDecoratorFile struct { + afero.File + fs *baseFileDecoratorFs +} + +func (l *baseFileDecoratorFile) Readdir(c int) (ofi []os.FileInfo, err error) { + dirnames, err := l.File.Readdirnames(c) + if err != nil { + return nil, err + } + + fisp := make([]os.FileInfo, 0, len(dirnames)) + + for _, dirname := range dirnames { + filename := dirname + + if l.Name() != "" && l.Name() != filepathSeparator { + filename = filepath.Join(l.Name(), dirname) + } + + // We need to resolve any symlink info. + fi, _, err := lstatIfPossible(l.fs.Fs, filename) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + fi, err = l.fs.decorate(fi, filename) + if err != nil { + return nil, errors.Wrap(err, "decorate") + } + fisp = append(fisp, fi) + } + + return fisp, err +} diff --git a/hugofs/fileinfo.go b/hugofs/fileinfo.go new file mode 100644 index 000000000..a2f12c429 --- /dev/null +++ b/hugofs/fileinfo.go @@ -0,0 +1,297 @@ +// Copyright 2019 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 hugofs provides the file systems used by Hugo. +package hugofs + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/gohugoio/hugo/hugofs/files" + "golang.org/x/text/unicode/norm" + + "github.com/pkg/errors" + + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/common/hreflect" + + "github.com/spf13/afero" +) + +const ( + metaKeyFilename = "filename" + metaKeyOriginalFilename = "originalFilename" + metaKeyName = "name" + metaKeyPath = "path" + metaKeyPathWalk = "pathWalk" + metaKeyLang = "lang" + metaKeyWeight = "weight" + metaKeyOrdinal = "ordinal" + metaKeyFs = "fs" + metaKeyOpener = "opener" + metaKeyIsOrdered = "isOrdered" + metaKeyIsSymlink = "isSymlink" + metaKeySkipDir = "skipDir" + metaKeyClassifier = "classifier" + metaKeyTranslationBaseName = "translationBaseName" + metaKeyTranslationBaseNameWithExt = "translationBaseNameWithExt" + metaKeyTranslations = "translations" + metaKeyDecoraterPath = "decoratorPath" +) + +type FileMeta map[string]interface{} + +func (f FileMeta) GetInt(key string) int { + return cast.ToInt(f[key]) +} + +func (f FileMeta) GetString(key string) string { + return cast.ToString(f[key]) +} + +func (f FileMeta) GetBool(key string) bool { + return cast.ToBool(f[key]) +} + +func (f FileMeta) Filename() string { + return f.stringV(metaKeyFilename) +} + +func (f FileMeta) OriginalFilename() string { + return f.stringV(metaKeyOriginalFilename) +} + +func (f FileMeta) SkipDir() bool { + return f.GetBool(metaKeySkipDir) +} +func (f FileMeta) TranslationBaseName() string { + return f.stringV(metaKeyTranslationBaseName) +} + +func (f FileMeta) TranslationBaseNameWithExt() string { + return f.stringV(metaKeyTranslationBaseNameWithExt) +} + +func (f FileMeta) Translations() []string { + return cast.ToStringSlice(f[metaKeyTranslations]) +} + +func (f FileMeta) Name() string { + return f.stringV(metaKeyName) +} + +func (f FileMeta) Classifier() string { + c := f.stringV(metaKeyClassifier) + if c != "" { + return c + } + + return files.ContentClassFile // For sorting +} + +func (f FileMeta) Lang() string { + return f.stringV(metaKeyLang) +} + +func (f FileMeta) Path() string { + return f.stringV(metaKeyPath) +} + +func (f FileMeta) Weight() int { + return f.GetInt(metaKeyWeight) +} + +func (f FileMeta) Ordinal() int { + return f.GetInt(metaKeyOrdinal) +} + +func (f FileMeta) IsOrdered() bool { + return f.GetBool(metaKeyIsOrdered) +} + +// IsSymlink returns whether this comes from a symlinked file or directory. +func (f FileMeta) IsSymlink() bool { + return f.GetBool(metaKeyIsSymlink) +} + +func (f FileMeta) Watch() bool { + if v, found := f["watch"]; found { + return v.(bool) + } + return false +} + +func (f FileMeta) Fs() afero.Fs { + if v, found := f[metaKeyFs]; found { + return v.(afero.Fs) + } + return nil +} + +func (f FileMeta) GetOpener() func() (afero.File, error) { + o, found := f[metaKeyOpener] + if !found { + return nil + } + return o.(func() (afero.File, error)) +} + +func (f FileMeta) Open() (afero.File, error) { + v, found := f[metaKeyOpener] + if !found { + return nil, errors.New("file opener not found") + } + return v.(func() (afero.File, error))() +} + +func (f FileMeta) stringV(key string) string { + if v, found := f[key]; found { + return v.(string) + } + return "" +} + +func (f FileMeta) setIfNotZero(key string, val interface{}) { + if !hreflect.IsTruthful(val) { + return + } + f[key] = val +} + +type FileMetaInfo interface { + os.FileInfo + Meta() FileMeta +} + +type fileInfoMeta struct { + os.FileInfo + m FileMeta +} + +func (fi *fileInfoMeta) Meta() FileMeta { + return fi.m +} + +func NewFileMetaInfo(fi os.FileInfo, m FileMeta) FileMetaInfo { + + if fim, ok := fi.(FileMetaInfo); ok { + mergeFileMeta(fim.Meta(), m) + } + return &fileInfoMeta{FileInfo: fi, m: m} +} + +// Merge metadata, last entry wins. +func mergeFileMeta(from, to FileMeta) { + if from == nil { + return + } + for k, v := range from { + if _, found := to[k]; !found { + to[k] = v + } + } +} + +type dirNameOnlyFileInfo struct { + name string +} + +func (fi *dirNameOnlyFileInfo) Name() string { + return fi.name +} + +func (fi *dirNameOnlyFileInfo) Size() int64 { + panic("not implemented") +} + +func (fi *dirNameOnlyFileInfo) Mode() os.FileMode { + return os.ModeDir +} + +func (fi *dirNameOnlyFileInfo) ModTime() time.Time { + return time.Time{} +} + +func (fi *dirNameOnlyFileInfo) IsDir() bool { + return true +} + +func (fi *dirNameOnlyFileInfo) Sys() interface{} { + return nil +} + +func newDirNameOnlyFileInfo(name string, isOrdered bool, fileOpener func() (afero.File, error)) FileMetaInfo { + name = normalizeFilename(name) + _, base := filepath.Split(name) + return NewFileMetaInfo(&dirNameOnlyFileInfo{name: base}, FileMeta{ + metaKeyFilename: name, + metaKeyIsOrdered: isOrdered, + metaKeyOpener: fileOpener}) +} + +func decorateFileInfo( + fi os.FileInfo, + fs afero.Fs, opener func() (afero.File, error), + filename, filepath string, inMeta FileMeta) FileMetaInfo { + + var meta FileMeta + var fim FileMetaInfo + + filepath = strings.TrimPrefix(filepath, filepathSeparator) + + var ok bool + if fim, ok = fi.(FileMetaInfo); ok { + meta = fim.Meta() + } else { + meta = make(FileMeta) + fim = NewFileMetaInfo(fi, meta) + } + + meta.setIfNotZero(metaKeyOpener, opener) + meta.setIfNotZero(metaKeyFs, fs) + meta.setIfNotZero(metaKeyPath, normalizeFilename(filepath)) + meta.setIfNotZero(metaKeyFilename, normalizeFilename(filename)) + + mergeFileMeta(inMeta, meta) + + return fim + +} + +func isSymlink(fi os.FileInfo) bool { + return fi != nil && fi.Mode()&os.ModeSymlink == os.ModeSymlink +} + +func fileInfosToFileMetaInfos(fis []os.FileInfo) []FileMetaInfo { + fims := make([]FileMetaInfo, len(fis)) + for i, v := range fis { + fims[i] = v.(FileMetaInfo) + } + return fims +} + +func normalizeFilename(filename string) string { + if filename == "" { + return "" + } + if runtime.GOOS == "darwin" { + // When a file system is HFS+, its filepath is in NFD form. + return norm.NFC.String(filename) + } + return filename +} diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go new file mode 100644 index 000000000..9aa2476b7 --- /dev/null +++ b/hugofs/files/classifier.go @@ -0,0 +1,121 @@ +// Copyright 2019 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 files + +import ( + "os" + "path/filepath" + "sort" + "strings" +) + +var ( + // This should be the only list of valid extensions for content files. + contentFileExtensions = []string{ + "html", "htm", + "mdown", "markdown", "md", + "asciidoc", "adoc", "ad", + "rest", "rst", + "mmark", + "org", + "pandoc", "pdc"} + + contentFileExtensionsSet map[string]bool +) + +func init() { + contentFileExtensionsSet = make(map[string]bool) + for _, ext := range contentFileExtensions { + contentFileExtensionsSet[ext] = true + } +} + +func IsContentFile(filename string) bool { + return contentFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")] +} + +func IsContentExt(ext string) bool { + return contentFileExtensionsSet[ext] +} + +const ( + ContentClassLeaf = "leaf" + ContentClassBranch = "branch" + ContentClassFile = "zfile" // Sort below + ContentClassContent = "zcontent" +) + +func ClassifyContentFile(filename string) string { + if !IsContentFile(filename) { + return ContentClassFile + } + if strings.HasPrefix(filename, "_index.") { + return ContentClassBranch + } + + if strings.HasPrefix(filename, "index.") { + return ContentClassLeaf + } + + return ContentClassContent +} + +const ( + ComponentFolderArchetypes = "archetypes" + ComponentFolderStatic = "static" + ComponentFolderLayouts = "layouts" + ComponentFolderContent = "content" + ComponentFolderData = "data" + ComponentFolderAssets = "assets" + ComponentFolderI18n = "i18n" + + FolderResources = "resources" +) + +var ( + ComponentFolders = []string{ + ComponentFolderArchetypes, + ComponentFolderStatic, + ComponentFolderLayouts, + ComponentFolderContent, + ComponentFolderData, + ComponentFolderAssets, + ComponentFolderI18n, + } + + componentFoldersSet = make(map[string]bool) +) + +func init() { + sort.Strings(ComponentFolders) + for _, f := range ComponentFolders { + componentFoldersSet[f] = true + } +} + +// ResolveComponentFolder returns "content" from "content/blog/foo.md" etc. +func ResolveComponentFolder(filename string) string { + filename = strings.TrimPrefix(filename, string(os.PathSeparator)) + for _, cf := range ComponentFolders { + if strings.HasPrefix(filename, cf) { + return cf + } + } + + return "" +} + +func IsComponentFolder(name string) bool { + return componentFoldersSet[name] +} diff --git a/hugofs/files/classifier_test.go b/hugofs/files/classifier_test.go new file mode 100644 index 000000000..d576b4e58 --- /dev/null +++ b/hugofs/files/classifier_test.go @@ -0,0 +1,49 @@ +// Copyright 2019 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 files + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsContentFile(t *testing.T) { + assert := require.New(t) + + assert.True(IsContentFile(filepath.FromSlash("my/file.md"))) + assert.True(IsContentFile(filepath.FromSlash("my/file.ad"))) + assert.False(IsContentFile(filepath.FromSlash("textfile.txt"))) + assert.True(IsContentExt("md")) + assert.False(IsContentExt("json")) +} + +func TestComponentFolders(t *testing.T) { + assert := require.New(t) + + // It's important that these are absolutely right and not changed. + assert.Equal(len(ComponentFolders), len(componentFoldersSet)) + assert.True(IsComponentFolder("archetypes")) + assert.True(IsComponentFolder("layouts")) + assert.True(IsComponentFolder("data")) + assert.True(IsComponentFolder("i18n")) + assert.True(IsComponentFolder("assets")) + assert.False(IsComponentFolder("resources")) + assert.True(IsComponentFolder("static")) + assert.True(IsComponentFolder("content")) + assert.False(IsComponentFolder("foo")) + assert.False(IsComponentFolder("")) + +} diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go new file mode 100644 index 000000000..952b276cf --- /dev/null +++ b/hugofs/filter_fs.go @@ -0,0 +1,341 @@ +// Copyright 2019 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 hugofs + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + "time" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/spf13/afero" +) + +var ( + _ afero.Fs = (*FilterFs)(nil) + _ afero.Lstater = (*FilterFs)(nil) + _ afero.File = (*filterDir)(nil) +) + +func NewLanguageFs(langs map[string]int, fs afero.Fs) (afero.Fs, error) { + + applyMeta := func(fs *FilterFs, name string, fis []os.FileInfo) { + + for i, fi := range fis { + if fi.IsDir() { + filename := filepath.Join(name, fi.Name()) + fis[i] = decorateFileInfo(fi, fs, fs.getOpener(filename), "", "", nil) + continue + } + + meta := fi.(FileMetaInfo).Meta() + lang := meta.Lang() + + fileLang, translationBaseName, translationBaseNameWithExt := langInfoFrom(langs, fi.Name()) + weight := 0 + + if fileLang != "" { + weight = 1 + if fileLang == lang { + // Give priority to myfile.sv.txt inside the sv filesystem. + weight++ + } + lang = fileLang + } + + fim := NewFileMetaInfo(fi, FileMeta{ + metaKeyLang: lang, + metaKeyWeight: weight, + metaKeyOrdinal: langs[lang], + metaKeyTranslationBaseName: translationBaseName, + metaKeyTranslationBaseNameWithExt: translationBaseNameWithExt, + metaKeyClassifier: files.ClassifyContentFile(fi.Name()), + }) + + fis[i] = fim + } + } + + all := func(fis []os.FileInfo) { + // Maps translation base name to a list of language codes. + translations := make(map[string][]string) + trackTranslation := func(meta FileMeta) { + name := meta.TranslationBaseNameWithExt() + translations[name] = append(translations[name], meta.Lang()) + } + for _, fi := range fis { + if fi.IsDir() { + continue + } + meta := fi.(FileMetaInfo).Meta() + + trackTranslation(meta) + + } + + for _, fi := range fis { + fim := fi.(FileMetaInfo) + langs := translations[fim.Meta().TranslationBaseNameWithExt()] + if len(langs) > 0 { + fim.Meta()["translations"] = sortAndremoveStringDuplicates(langs)< |