forked from robfig/soy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bundle.go
223 lines (199 loc) · 5.85 KB
/
bundle.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
package soy
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/yext/soy/data"
"github.com/yext/soy/parse"
"github.com/yext/soy/parsepasses"
"github.com/yext/soy/soyhtml"
"github.com/yext/soy/template"
)
// Logger is used to print notifications and compile errors when using the
// "WatchFiles" feature.
var Logger = log.New(os.Stderr, "[soy] ", 0)
type soyFile struct{ name, content string }
// Bundle is a collection of Soy content (templates and globals). It acts as
// input for the Soy compiler.
type Bundle struct {
files []soyFile
globals data.Map
err error
watcher *fsnotify.Watcher
parsepasses []func(template.Registry) error
recompilationCallback func(*template.Registry)
}
// NewBundle returns an empty bundle.
func NewBundle() *Bundle {
return &Bundle{globals: make(data.Map)}
}
// WatchFiles tells Soy to watch any template files added to this bundle,
// re-compile as necessary, and propagate the updates to your tofu. It should
// be called once, before adding any files.
func (b *Bundle) WatchFiles(watch bool) *Bundle {
if watch && b.err == nil && b.watcher == nil {
b.watcher, b.err = fsnotify.NewWatcher()
}
return b
}
// AddTemplateDir adds all *.soy files found within the given directory
// (including sub-directories) to the bundle.
func (b *Bundle) AddTemplateDir(root string) *Bundle {
var err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".soy") {
return nil
}
b.AddTemplateFile(path)
return nil
})
if err != nil {
b.err = err
}
return b
}
// AddTemplateFile adds the given Soy template file text to this bundle.
// If WatchFiles is on, it will be subsequently watched for updates.
func (b *Bundle) AddTemplateFile(filename string) *Bundle {
content, err := ioutil.ReadFile(filename)
if err != nil {
b.err = err
}
if b.err == nil && b.watcher != nil {
b.err = b.watcher.Add(filename)
}
return b.AddTemplateString(filename, string(content))
}
// AddTemplateString adds the given template to the bundle. The name is only
// used for error messages - it does not need to be provided nor does it need to
// be a real filename.
func (b *Bundle) AddTemplateString(filename, soyfile string) *Bundle {
b.files = append(b.files, soyFile{filename, soyfile})
return b
}
// AddGlobalsFile opens and parses the given filename for Soy globals, and adds
// the resulting data map to the bundle.
func (b *Bundle) AddGlobalsFile(filename string) *Bundle {
var f, err = os.Open(filename)
if err != nil {
b.err = err
return b
}
globals, err := ParseGlobals(f)
if err != nil {
b.err = err
}
f.Close()
return b.AddGlobalsMap(globals)
}
func (b *Bundle) AddGlobalsMap(globals data.Map) *Bundle {
for k, v := range globals {
if existing, ok := b.globals[k]; ok {
b.err = fmt.Errorf("global %q already defined as %q", k, existing)
return b
}
b.globals[k] = v
}
return b
}
// SetRecompilationCallback assigns the bundle a function to call after
// recompilation. This is called before updating the in-use registry.
func (b *Bundle) SetRecompilationCallback(c func(*template.Registry)) *Bundle {
b.recompilationCallback = c
return b
}
// AddParsePass adds a function to the bundle that will be called
// after the Soy is parsed.
func (b *Bundle) AddParsePass(f func(template.Registry) error) *Bundle {
b.parsepasses = append(b.parsepasses, f)
return b
}
// Compile parses all of the Soy files in this bundle, verifies a number of
// rules about data references, and returns the completed template registry.
func (b *Bundle) Compile() (*template.Registry, error) {
if b.err != nil {
return nil, b.err
}
// Compile all the Soy (globals are already parsed).
var registry = template.Registry{}
for _, soyfile := range b.files {
var tree, err = parse.SoyFile(soyfile.name, soyfile.content)
if err != nil {
return nil, err
}
if err = registry.Add(tree); err != nil {
return nil, err
}
}
// Apply the post-parse processing
for _, parsepass := range b.parsepasses {
if err := parsepass(registry); err != nil {
return nil, err
}
}
if err := parsepasses.CheckDataRefs(registry); err != nil {
return nil, err
}
if err := parsepasses.SetGlobals(registry, b.globals); err != nil {
return nil, err
}
parsepasses.ProcessMessages(registry)
if b.watcher != nil {
go b.recompiler(®istry)
}
return ®istry, nil
}
// CompileToTofu returns a soyhtml.Tofu object that allows you to render soy
// templates to HTML.
func (b *Bundle) CompileToTofu() (*soyhtml.Tofu, error) {
var registry, err = b.Compile()
// TODO: Verify all used funcs exist and have the right # args.
return soyhtml.NewTofu(registry), err
}
func (b *Bundle) recompiler(reg *template.Registry) {
for {
select {
case ev := <-b.watcher.Events:
// If it's a rename, then fsnotify has removed the watch.
// Add it back, after a delay.
if ev.Op == fsnotify.Rename || ev.Op == fsnotify.Remove {
time.Sleep(10 * time.Millisecond)
if err := b.watcher.Add(ev.Name); err != nil {
Logger.Println(err)
}
}
// Recompile all the Soy.
var bundle = NewBundle().
AddGlobalsMap(b.globals)
for _, soyfile := range b.files {
bundle.AddTemplateFile(soyfile.name)
}
var registry, err = bundle.Compile()
if err != nil {
Logger.Println(err)
continue
}
if b.recompilationCallback != nil {
b.recompilationCallback(registry)
}
// update the existing template registry.
// (this is not goroutine-safe, but that seems ok for a development aid,
// as long as it works in practice)
*reg = *registry
Logger.Printf("update successful (%v)", ev)
case err := <-b.watcher.Errors:
// Nothing to do with errors
Logger.Println(err)
}
}
}