-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtemplate.go
More file actions
319 lines (269 loc) · 8.58 KB
/
Copy pathtemplate.go
File metadata and controls
319 lines (269 loc) · 8.58 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
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package vuego
import (
"context"
"fmt"
"io"
"io/fs"
"path/filepath"
"strings"
yaml "gopkg.in/yaml.v3"
"github.com/titpetric/vuego/internal/helpers"
)
// LoadOption is a functional option for configuring Load().
type LoadOption func(*Vue)
// New creates a new Template for rendering strings, bytes, or readers without a filesystem.
// Use this when templates are provided as strings/bytes rather than loaded from files.
// To render from files, use NewFS(fs) or New(WithFS(fs)) instead.
// The returned Template can be used for variable assignment and rendering.
func New(opts ...LoadOption) Template {
return NewFS(nil, opts...)
}
// View is a type safety shim to bind a template file to a data model type.
// The returned template should be rendered and discarded.
func View[V any](renderer Template, filename string, data V) Template {
return renderer.Load(filename).Fill(data)
}
// WithFS returns a LoadOption that sets the filesystem for template loading.
func WithFS(templateFS fs.FS) LoadOption {
return func(vue *Vue) {
vue.templateFS = templateFS
vue.loader = NewLoader(templateFS)
}
}
// WithFuncs returns a LoadOption that merges custom template functions into the existing funcmap.
func WithFuncs(funcMap FuncMap) LoadOption {
return func(vue *Vue) {
vue.Funcs(funcMap)
}
}
// Template represents a prepared vuego template.
// It allows variable assignment and rendering with internal buffering.
type Template interface {
TemplateConstructors
TemplateState
TemplateRendering
TemplateRenderingDetail
}
// TemplateConstructors creates new template rendering contexts.
// The results provide request scoped data allocations and should be discarded after use.
type TemplateConstructors interface {
New() Template
Load(filename string) Template
}
// TemplateState bundles the interface for template state management.
type TemplateState interface {
Fill(vars any) Template
Assign(key string, value any) Template
Get(key string) string
}
// TemplateRendering bundles the interface for the render functions.
type TemplateRendering interface {
Render(ctx context.Context, w io.Writer) error
}
// TemplateRenderingDetail the interface for stateless render functions.
type TemplateRenderingDetail interface {
RenderFile(ctx context.Context, w io.Writer, filename string) error
RenderString(ctx context.Context, w io.Writer, templateStr string) error
RenderByte(ctx context.Context, w io.Writer, templateData []byte) error
RenderReader(ctx context.Context, w io.Writer, r io.Reader) error
}
// template is the internal implementation of Template.
type template struct {
vue *Vue
stack *Stack
// filename and error for Template.Load
err error
frontMatter map[string]any
templateBytes []byte
filename string
filenameLoaded bool
}
var _ Template = &template{}
// NewFS creates a new Template with access to the given filesystem and optional configurations.
// The returned Template can be used to render files, strings, or bytes with variable assignment.
func NewFS(templateFS fs.FS, opts ...LoadOption) Template {
vue := NewVue(templateFS)
// Apply functional options
for _, opt := range opts {
opt(vue)
}
// Implicitly load config (theme.yml + data/*.yml) from the filesystem
loadConfig(vue)
tpl := &template{
vue: vue,
stack: NewStack(nil),
}
// Apply initial data if available (equivalent to Fill before New/Load)
if vue.initialData != nil {
tpl.Fill(vue.initialData)
}
return tpl
}
// New will create an empty loaded copy safe for concurrent use.
// It provides an implementation of Template but provides a typed return
// that's open for further modification in Load().
func (t *template) New() Template {
return t.new()
}
func (t *template) new() *template {
return &template{
stack: t.stack.Copy(),
vue: t.vue,
}
}
// WithLessProcessor returns a LoadOption that registers a LESS processor.
func WithLessProcessor() LoadOption {
return func(vue *Vue) {
vue.RegisterNodeProcessor(NewLessProcessor(vue.templateFS))
}
}
// WithProcessor returns a LoadOption that registers a custom node processor.
func WithProcessor(processor NodeProcessor) LoadOption {
return func(vue *Vue) {
vue.RegisterNodeProcessor(processor)
}
}
// WithComponents returns a LoadOption that registers all component shorthands.
// It recursively loads all .vuego files from the components folder and subfolders.
// File paths are mapped to kebab-case tag names using directory path and filename.
func WithComponents() LoadOption {
return func(vue *Vue) {
// Walk the components directory recursively
err := fs.WalkDir(vue.templateFS, "components", func(path string, d fs.DirEntry, err error) error {
if err != nil {
// Skip directories that don't exist or can't be accessed
return nil
}
// Skip directories, only process files
if d.IsDir() {
return nil
}
// Only process .vuego files
if !strings.HasSuffix(d.Name(), ".vuego") {
return nil
}
// Extract the relative path from components folder
relPath := strings.TrimPrefix(path, "components/")
relPath = strings.TrimSuffix(relPath, ".vuego")
// Convert to kebab-case for the tag name
// Split by path separators and convert each part
parts := strings.Split(relPath, string(filepath.Separator))
for i, part := range parts {
parts[i] = helpers.CamelToKebab(part)
}
tagName := strings.Join(parts, "-")
// Store the mapping
vue.RegisterComponent(tagName, path)
return nil
})
// Silently ignore if components directory doesn't exist
_ = err
}
}
// loadConfig loads YAML config files from the filesystem into vue.initialData.
// It loads root-level theme.yml, then all YAML files from the data/ directory.
// Files in data/ override root-level values with the same keys.
func loadConfig(vue *Vue) {
if vue.templateFS == nil {
return
}
if vue.initialData == nil {
vue.initialData = make(map[string]any)
}
// Helper to load and merge a YAML file
loadYAML := func(path string) {
content, err := fs.ReadFile(vue.templateFS, path)
if err != nil {
return // File doesn't exist or can't be read, skip silently
}
var data map[string]any
if err := yaml.Unmarshal(content, &data); err != nil {
return // Invalid YAML, skip silently
}
// Merge into initialData
for k, v := range data {
vue.initialData[k] = v
}
}
// Load root-level theme.yml first
loadYAML("theme.yml")
// Load all YAML files from data/ directory (overrides root-level values)
entries, err := fs.ReadDir(vue.templateFS, "data")
if err != nil {
return // data/ directory doesn't exist, skip silently
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
loadYAML("data/" + name)
}
}
}
// Fill sets all variables from the map, preserving any front-matter that was loaded.
func (t *template) Fill(vars any) Template {
// Start with auto-loaded config (lowest precedence)
dataMap := map[string]any{}
if t.vue.initialData != nil {
for k, v := range t.vue.initialData {
dataMap[k] = v
}
}
// Merge passed data (overrides config)
passedData := toMapData(vars)
for k, v := range passedData {
dataMap[k] = v
}
// Merge loaded front-matter into data (front-matter takes precedence)
for k, v := range t.frontMatter {
dataMap[k] = v
}
t.stack = NewStackWithData(dataMap, vars)
return t
}
// Assign sets a single variable.
func (t *template) Assign(key string, value any) Template {
t.stack.Set(key, value)
return t
}
// Err returns the internal error if any.
func (t *template) Err() error {
return t.err
}
// SetErr sets the internal error. Used from Load().
func (t *template) SetErr(err error) {
t.err = err
}
// Load will load a template file and front matter, extending VueContext with the loaded DOM.
// Front matter data is merged into the template's data and available via Get().
//
// Load returns a new allocation from a base template which may have data filled.
// The returned value is a throw-away after Render() is invoked.
func (t *template) Load(filename string) Template {
tpl := t.new()
// Load the template with front-matter and raw template bytes
tpl.frontMatter, tpl.templateBytes, tpl.err = t.vue.loader.loadFragment(filename)
tpl.filename = filename
tpl.filenameLoaded = true
for k, v := range tpl.frontMatter {
tpl.Assign(k, v)
}
return tpl
}
// Get retrieves a variable value as a string.
func (t *template) Get(key string) string {
val, ok := t.stack.Lookup(key)
if !ok || val == nil {
return ""
}
if v, ok := val.(string); ok {
return v
}
if v, ok := val.(bool); ok {
return fmt.Sprint(v)
}
result := fmt.Sprint(val)
return result
}