-
Notifications
You must be signed in to change notification settings - Fork 0
/
views.go
223 lines (211 loc) · 6.64 KB
/
views.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 treetop
// View is used to define hierarchies of nested template-handler pairs
// so that HTTP endpoints can be constructed for different page configurations.
//
// A 'View' is a template string (usually file path) paired with a handler function.
// Go templates can contain named nested blocks. Defining a 'SubView' associates
// a handler and a template with a block embedded within a parent template.
// HTTP handlers can then be constructed for various page configurations.
//
// Example of a basic template hierarchy
//
// baseHandler(...)
// | base.html ========================|
// | … |
// | {{ template "content" .Content }} |
// | … ^ |
// |_________________|_________________|
// |
// ______/ \______
// contentAHandler(...) contentBHandler(...)
// | contentA.html ========== | | contentB.html ========== |
// | | | |
// | {{ block "content" . }}… | | {{ block "content" . }}… |
// |__________________________| |__________________________|
//
// Pseudo request and response:
//
// GET /path/to/a
// > HTTP/1.1 200 OK
// > ... base.html { Content: contentA.html }
//
// GET /path/to/b
// > HTTP/1.1 200 OK
// > ... base.html { Content: contentB.html }
//
//
// Example of using the library to bind constructed handlers to a HTTP router.
//
// base := treetop.NewView(
// "base.html",
// baseHandler,
// )
//
// contentA := base.NewSubView(
// "content",
// "contentA.html",
// contentAHandler,
// )
//
// contentB := base.NewSubView(
// "content",
// "contentB.html",
// contentBHandler,
// )
//
// exec := treetop.FileExecutor{}
// mymux.Handle("/path/to/a", exec.ViewHandler(contentA))
// mymux.Handle("/path/to/b", exec.ViewHandler(contentB))
//
//
// This is useful for creating Treetop enabled endpoints because the constructed handler
// is capable of loading either a full page or just the "content" part of the page depending
// upon the request.
//
type View struct {
Template string
HandlerFunc ViewHandlerFunc
SubViews map[string]*View
Defines string
Parent *View
}
// NewView creates an instance of a view given a template + handler pair
func NewView(tmpl string, handler ViewHandlerFunc) *View {
return &View{
Template: tmpl,
HandlerFunc: handler,
SubViews: make(map[string]*View),
}
}
// NewSubView creates an instance of a view given a template + handler pair
// this view is a detached subview, in that is does not reference a parent
func NewSubView(defines, tmpl string, handler ViewHandlerFunc) *View {
v := NewView(tmpl, handler)
v.Defines = defines
return v
}
// NewSubView create a new view extending a named block within the current view
func (v *View) NewSubView(defines string, tmpl string, handler ViewHandlerFunc) *View {
sub := NewView(tmpl, handler)
sub.Defines = defines
sub.Parent = v
if _, ok := v.SubViews[defines]; !ok {
v.SubViews[defines] = nil
}
return sub
}
// NewDefaultSubView create a new view extending a named block within the current view
// and updates the parent to use this view by default
func (v *View) NewDefaultSubView(defines string, tmpl string, handler ViewHandlerFunc) *View {
sub := v.NewSubView(defines, tmpl, handler)
v.SubViews[defines] = sub
return sub
}
// HasSubView asserts that a subview name exists without the need to define a template
func (v *View) HasSubView(name string) {
if _, ok := v.SubViews[name]; !ok {
v.SubViews[name] = nil
}
}
// Copy creates a duplicate so that the original is not affected by
// changes. This will propegate as a 'deep copy' to all default subviews
func (v *View) Copy() *View {
if v == nil {
return nil
}
copy := NewView(v.Template, v.HandlerFunc)
copy.Defines = v.Defines
copy.Parent = v.Parent
for name, sub := range v.SubViews {
copy.SubViews[name] = sub.Copy()
}
return copy
}
// CompileViews is used to create an endpoint configuration combining supplied view
// definitions based upon the template names they define.
//
// This returns:
// - a full-page view instance,
// - a partial page view instance, and
// - any disconnect fragment views that should be appended to partial requests.
//
func CompileViews(view *View, includes ...*View) (page, part *View, postscript []*View) {
if view == nil {
return nil, nil, nil
}
// Merge the includes and the view where possible.
// Views to the left 'consume' those to the right when a match is found.
// 'Postscripts' are includes that could not be merged.
part = view.Copy()
{
consumed := make([]bool, len(includes))
var found bool
for i, incl := range includes {
part, found = insertView(part, incl)
consumed[i] = found
}
for i := range includes {
if i >= len(includes)-1 {
break
}
for j := range includes[i+1:] {
includes[i], found = insertView(includes[i], includes[i+j+1])
consumed[i] = found || consumed[i]
}
}
for i := range includes {
if !consumed[i] {
postscript = append(postscript, includes[i])
}
}
}
// constructing the 'page' involes modifying the series of parents
// to ensure that this view is reachable from the root (hence the copying).
// The modified root is our page
root := view
for root.Parent != nil {
// make a copy of the parent and ensure that it points to
// the child sub view
pCopy := root.Parent.Copy()
pCopy.SubViews[root.Defines] = root
root = pCopy
}
if root != view {
page, _ = insertView(root, view)
} else {
page = view
}
for _, incl := range includes {
page, _ = insertView(page, incl)
}
return page, part, postscript
}
// insertView attempts to incorporate the child into the template hierarchy of this view.
// If a match is found for the definition name, views will be copied & modified as necessary and
// a flag is returned to indicate whether a match was found.
//
func insertView(view, child *View) (*View, bool) {
if view == nil {
return nil, false
}
if child == nil || child.Defines == "" || len(view.SubViews) == 0 {
return view, false
}
if _, found := view.SubViews[child.Defines]; found {
copy := view.Copy()
copy.SubViews[child.Defines] = child
return copy, true
}
// At this point a match was not found directly in this view,
// Attempt to apply the child to reachable subviews
//
for _, sub := range view.SubViews {
copiedSub, found := insertView(sub, child)
if found {
copy := view.Copy()
copy.SubViews[copiedSub.Defines] = copiedSub
return copy, true
}
}
return view, false
}