-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfs.go
More file actions
164 lines (147 loc) · 4.47 KB
/
fs.go
File metadata and controls
164 lines (147 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package templar
import (
"fmt"
"io/fs"
"log/slog"
"path"
"strings"
)
// FSFolder pairs a filesystem with a folder path within it.
type FSFolder struct {
FS fs.FS // filesystem to search in
Path string // folder path within the FS
}
// FileSystemLoader loads templates from one or more filesystem folders.
// Each folder is backed by an fs.FS — use NewLocalFS for local disk, NewMemFS for tests.
type FileSystemLoader struct {
// Folders is the list of FS+path pairs to search for templates.
Folders []FSFolder
// Extensions is a list of file extensions to consider as templates.
Extensions []string
}
// NewFileSystemLoader creates a loader that searches the given FS+path pairs.
// Default extensions: .tmpl, .tmplus, .html.
func NewFileSystemLoader(folders ...FSFolder) *FileSystemLoader {
return &FileSystemLoader{
Folders: folders,
Extensions: []string{
"tmpl", "tmplus", "html",
},
}
}
// LocalFolder is a convenience for creating an FSFolder from a local directory path.
func LocalFolder(dir string) FSFolder {
return FSFolder{FS: NewLocalFS(dir), Path: "."}
}
// Load attempts to find and load a template with the given name.
func (g *FileSystemLoader) Load(name string, cwd string) (template []*Template, err error) {
ext := path.Ext(name)
extensions := g.Extensions
withoutext := name
if ext != "" {
extensions = []string{ext[1:]}
withoutext = name[:len(name)-len(ext)]
}
isRelative := strings.HasPrefix(name, "./") || strings.HasPrefix(name, "../")
entries := g.Folders
if cwd != "" {
// cwd is always an FS path — find which folder's FS it belongs to, or assume first
cwdEntry := FSFolder{Path: cwd}
if len(g.Folders) > 0 {
cwdEntry.FS = g.Folders[0].FS
}
if isRelative {
entries = []FSFolder{cwdEntry}
} else {
entries = append(append([]FSFolder{}, entries...), cwdEntry)
}
}
for _, entry := range entries {
if !g.folderExists(entry) {
continue
}
for _, ext := range extensions {
withext := fmt.Sprintf("%s.%s", withoutext, ext)
contents, fullPath, err := g.readTemplate(entry, withext)
if err != nil {
continue
}
return []*Template{{RawSource: contents, Path: fullPath}}, nil
}
}
slog.Warn("Template not found", "name", name, "cwd", cwd)
return nil, TemplateNotFound
}
// resolve ensures FSFolder has an FS set — defaults to LocalFS if nil.
func (entry *FSFolder) resolve() {
if entry.FS == nil {
entry.FS = NewLocalFS(entry.Path)
entry.Path = "."
}
}
// folderExists checks if a folder exists in its FS.
func (g *FileSystemLoader) folderExists(entry FSFolder) bool {
entry.resolve()
info, err := fs.Stat(entry.FS, entry.Path)
if err != nil {
// "." always exists conceptually
if entry.Path == "." || entry.Path == "" {
return true
}
slog.Debug("folder does not exist", "folder", entry.Path)
return false
}
return info.IsDir()
}
// readTemplate reads a template file from an FSFolder.
func (g *FileSystemLoader) readTemplate(entry FSFolder, name string) ([]byte, string, error) {
entry.resolve()
fpath := name
if entry.Path != "" && entry.Path != "." {
fpath = entry.Path + "/" + name
}
data, err := fs.ReadFile(entry.FS, fpath)
if err != nil {
return nil, "", err
}
return data, fpath, nil
}
// LoaderList is a composite loader that tries multiple loaders in sequence
// and returns the first successful match.
type LoaderList struct {
// DefaultLoader is used as a fallback if no other loaders succeed.
DefaultLoader TemplateLoader
// loaders is the ordered list of template loaders to try.
loaders []TemplateLoader
}
// AddLoader adds a new loader to the list of loaders to try.
func (t *LoaderList) AddLoader(loader TemplateLoader) *LoaderList {
t.loaders = append(t.loaders, loader)
return t
}
// Load attempts to load a template with the given name by trying each loader in sequence.
func (t *LoaderList) Load(name string, cwd string) (matched []*Template, err error) {
for _, loader := range t.loaders {
matched, err = loader.Load(name, cwd)
if err == nil && matched != nil && len(matched) > 0 {
return matched, err
} else if err == TemplateNotFound {
continue
} else {
break
}
}
if t.DefaultLoader != nil {
return t.DefaultLoader.Load(name, cwd)
}
return nil, TemplateNotFound
}
// LocalFolders converts a list of directory paths to FSFolder entries.
// Convenience for migrating code that passes string paths.
func LocalFolders(dirs ...string) []FSFolder {
var folders []FSFolder
for _, d := range dirs {
folders = append(folders, LocalFolder(d))
}
return folders
}