-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathparse.go
231 lines (195 loc) · 6.47 KB
/
parse.go
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Package recurparse parsing go templates recursively, instead of default template behavior
// that puts all files together.
//
// It goes through subfolders recursively and parses the files matching the glob.
// The templates have the subfolder path in the name, separated by forward slash (even on windows).
//
// The template names are as relative to the given folder.
//
// All the 4 functions behave in similar way.
//
// If the first argument is nil,
// the resulting template will have one of the files as name and content;
// if it's an existing template, it will add the files as associated templates.
//
// The pattern works only on the final filename; that is, k*.html will match foo/bar/kxxx.html;
// it does NOT filter the directory name, all directories are walked through.
//
// The matching logic is using filepath.Match on the filename, in the same way template.Parse do it.
// It follows all symlinks, the symlinks will be there under the symlink name.
// If there is a "symlink loop" (that is, symlink to .. or similar), the function will panic and run out of memory.
//
// If there is no files that matches, the function errors, same as go's ParseFiles.
package recurparse
import (
"fmt"
templateHtml "html/template"
templateText "text/template"
"io/fs"
"os"
"path/filepath"
)
// named templ, as `template` is import name in tests
// this is all we actually need for template type
type templ interface {
comparable
Name() string
}
// this is abstraction of top-level template functions, so I can reuse same functions
type templateCreator[T templ] interface {
New(name string) T
NewBasedOn(nameGiver T, name string) T
Parse(nameGiver T, text string) (T, error)
Nil() T
}
type htmlTemplateCreator struct{}
func (htmlTemplateCreator) New(name string) *templateHtml.Template {
return templateHtml.New(name)
}
func (htmlTemplateCreator) NewBasedOn(t *templateHtml.Template, name string) *templateHtml.Template {
return t.New(name)
}
func (htmlTemplateCreator) Parse(t *templateHtml.Template, text string) (*templateHtml.Template, error) {
return t.Parse(text)
}
func (htmlTemplateCreator) Nil() *templateHtml.Template {
return nil
}
type textTemplateCreator struct{}
func (textTemplateCreator) New(name string) *templateText.Template {
return templateText.New(name)
}
func (textTemplateCreator) NewBasedOn(t *templateText.Template, name string) *templateText.Template {
return t.New(name)
}
func (textTemplateCreator) Parse(t *templateText.Template, text string) (*templateText.Template, error) {
return t.Parse(text)
}
func (textTemplateCreator) Nil() *templateText.Template {
return nil
}
func parseFS[T templ](t T, creator templateCreator[T], fsys fs.FS, glob string) (T, error) {
// first we get the names
n := creator.Nil()
files, err := matchingNames(fsys, glob)
if err != nil {
return n, err
}
if len(files) == 0 {
// Not really a problem, but be consistent with ParseFiles
return n, fmt.Errorf("recurparse: no files matched")
}
// now parse the templates.
// the actual code just logic copied from src/html/template/helper.go, just changed for our purposes
for _, filename := range files {
b, err := fs.ReadFile(fsys, filename)
if err != nil {
return n, fmt.Errorf("recurparse: cannot read %q: %w", filename, err)
}
s := string(b)
// this is copied verbatim from go template.. I always found the rewrite logic a bit confusing,
// but it is what it is. Let's keep the logic.
if t == n {
t = creator.New(filename)
}
var tmpl T
if filename == t.Name() {
tmpl = t
} else {
tmpl = creator.NewBasedOn(t, filename)
}
_, err = creator.Parse(tmpl, s)
if err != nil {
return n, fmt.Errorf("recurparse: cannot parse %q: %w", filename, err)
}
}
return t, nil
}
// TextParseFS opens a fs.FS filesystem and recursively parses the files there as text templates.
//
// See package docs for details of the behavior.
func TextParseFS(t *templateText.Template, fsys fs.FS, glob string) (*templateText.Template, error) {
return parseFS[*templateText.Template](t, textTemplateCreator{}, fsys, glob)
}
// TextParse opens a directory and recursively parses the files there as text templates.
//
// See package docs for details of the behavior.
func TextParse(t *templateText.Template, dirPath, glob string) (*templateText.Template, error) {
resolved, err := filepath.EvalSymlinks(dirPath)
if err != nil {
return nil, fmt.Errorf("recurparse: cannot resolve %q (%w)", dirPath, err)
}
fsys := os.DirFS(resolved)
return TextParseFS(t, fsys, glob)
}
// HTMLParseFS opens a fs.FS filesystem and recursively parses the files there as HTML templates.
//
// See package docs for details of the behavior.
func HTMLParseFS(t *templateHtml.Template, fsys fs.FS, glob string) (*templateHtml.Template, error) {
return parseFS[*templateHtml.Template](t, htmlTemplateCreator{}, fsys, glob)
}
// HTMLParse opens a fs.FS filesystem and recursively parses the files there as HTML templates.
//
// See package docs for details of the behavior.
func HTMLParse(t *templateHtml.Template, dirPath, glob string) (*templateHtml.Template, error) {
resolved, err := filepath.EvalSymlinks(dirPath)
if err != nil {
return nil, fmt.Errorf("recurparse: cannot resolve %q (%w)", dirPath, err)
}
fsys := os.DirFS(resolved)
return HTMLParseFS(t, fsys, glob)
}
// matchingNames is where we walk through the FS and actually get the names
func matchingNames(myfs fs.FS, glob string) ([]string, error) {
isSymlink := func(d fs.DirEntry) (bool, error) {
info, err := d.Info()
if err != nil {
return false, err
}
return info.Mode()&os.ModeSymlink != 0, nil
}
var matched []string
var walk func(dir string) error
walk = func(dir string) error {
err := fs.WalkDir(myfs, dir, func(path string, d fs.DirEntry, err error) error {
// we return errors everywhere we can... not sure if it's the best idea,
// but I guess better error than not? be safe
if err != nil {
return err
}
if !d.IsDir() {
isMatched, err := filepath.Match(glob, d.Name())
if err != nil {
return err
}
if isMatched {
matched = append(matched, path)
} else {
sym, err := isSymlink(d)
if err != nil {
return err
}
if sym {
fsStat, err := fs.Stat(myfs, path)
if err == nil {
if err != nil {
return err
}
isDir := fsStat.IsDir()
if isDir {
err := walk(path)
if err != nil {
return err
}
}
}
}
}
}
return nil
})
return err
}
err := walk(".")
return matched, err
}