/
hweb.go
375 lines (352 loc) · 10.8 KB
/
hweb.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
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// Copyright 2018 NDP Systèmes. All Rights Reserved.
// See LICENSE file for full licensing details.
// Package hweb provides utilities for the HWeb templating system
// such as transpilation to Pongo2 templates.
package hweb
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/beevik/etree"
"github.com/erlangs/okoo/src/tools/xmlutils"
"github.com/flosch/pongo2"
)
// TemplateSet allows you to create your own group of templates with their own
// global context (which is shared among all members of the set) and their own
// configuration.
// It's useful for a separation of different kind of templates
// (e. g. web templates vs. mail templates).
type TemplateSet = pongo2.TemplateSet
// A Template to be rendered with server's c.HTML()
type Template = pongo2.Template
// The Context with which to render a HWeb template
type Context = pongo2.Context
// NewSet can be used to create sets with different kind of templates
// (e. g. web from mail templates), with different globals or
// other configurations.
func NewSet(name string, loader pongo2.TemplateLoader) *TemplateSet {
return pongo2.NewSet(name, loader)
}
// Must panics, if a Template couldn't successfully parsed. This is how you
// would use it:
// var baseTemplate = pongo2.Must(pongo2.FromFile("templates/base.html"))
func Must(tpl *Template, err error) *Template {
return pongo2.Must(tpl, err)
}
// ToPongo transpiles the HWeb src template to Pongo2 template
func ToPongo(src []byte) ([]byte, error) {
doc, err := xmlutils.XMLToDocument(string(src))
if err != nil {
return nil, err
}
doc.InsertChild(doc.Child[0], &etree.CharData{Data: "{% set _1 = _0 %}"})
transpileOutput(doc.ChildElements())
if err = transpileAttributes(doc.ChildElements()); err != nil {
return nil, err
}
if err = transpileConditionals(doc.ChildElements()); err != nil {
return nil, err
}
if err = transpileLoops(doc.ChildElements()); err != nil {
return nil, err
}
if err = transpileCalls(doc.ChildElements()); err != nil {
return nil, err
}
if err = transpileVariables(doc.ChildElements()); err != nil {
return nil, err
}
transpileSmartFields(doc.ChildElements())
doc.WriteSettings.CanonicalText = true
res, err := doc.WriteToBytes()
if err != nil {
return nil, err
}
res = unescapeXMLEntities(res)
return res, nil
}
// escapeXMLEntities mark xml entities as __[entity]__ so that they can be
// unescaped after XML rendering to valid pongo with unescapeXMLEntities.
//
// This function should be called on text in Pongo2 tags
func escapeXMLEntities(src string) string {
res := strings.Replace(src, ">", ">", -1)
res = strings.Replace(res, "<", "<", -1)
res = strings.Replace(res, "\"", """, -1)
res = strings.Replace(res, "'", "'", -1)
res = strings.Replace(res, ">", "__>__", -1)
res = strings.Replace(res, "<", "__<__", -1)
res = strings.Replace(res, """, "__"__", -1)
res = strings.Replace(res, "'", "__'__", -1)
return res
}
// unescapeXMLEntities takes marked entities (as __[entity]__) and
// replace them with the real character.
func unescapeXMLEntities(src []byte) []byte {
res := bytes.Replace(src, []byte("__&gt;__"), []byte(">"), -1)
res = bytes.Replace(res, []byte("__&lt;__"), []byte("<"), -1)
res = bytes.Replace(res, []byte("__&quot;__"), []byte("\""), -1)
res = bytes.Replace(res, []byte("__&apos;__"), []byte("'"), -1)
return res
}
// replaceTag replaces the given xml tag in its parents children by the given
// django directive dir. If the xml tag is not t, it removes attributes given
// by attrs
func replaceTag(el *etree.Element, attrs []string, dir string) {
cd := &etree.CharData{Data: dir}
switch el.Tag {
case "t":
el.Parent().InsertChild(el, cd)
for len(el.Child) != 0 {
extractToParent(el.Child[0])
}
el.Parent().RemoveChild(el)
default:
el.Parent().InsertChild(el, cd)
for _, key := range attrs {
el.RemoveAttr(key)
}
}
}
// extractToParent extracts this token and
// attach it to its grand parent, just before its parent.
// If tok has no grand parent, just leave it where it is.
func extractToParent(tok etree.Token) {
if tok.Parent().Parent() == nil {
return
}
tok.Parent().Parent().InsertChild(tok.Parent(), tok)
}
// transpileOutput modifies given elements with t-esc or t-raw tags
func transpileOutput(elts []*etree.Element) {
for _, elt := range elts {
transpileOutput(elt.ChildElements())
for _, attr := range elt.Attr {
var format string
switch attr.Key {
case "t-esc":
format = "{{ %s }}"
case "t-raw":
format = "{{ %s|safe }}"
default:
continue
}
val := attr.Value
if val == "0" {
val = "_1"
}
text := fmt.Sprintf(format, val)
switch elt.Tag {
case "t":
cd := &etree.CharData{Data: text}
elt.Parent().InsertChild(elt, cd)
elt.Parent().RemoveChild(elt)
default:
elt.RemoveAttr(attr.Key)
elt.SetText(text)
}
}
}
}
// transpileAttributes modifies dynamic attributes of all given elements,
// i.e. t-att, t-att-xxx, t-attf-yyy
func transpileAttributes(elts []*etree.Element) error {
for _, elt := range elts {
attrs := make([]etree.Attr, len(elt.Attr))
copy(attrs, elt.Attr)
for _, attr := range attrs {
switch {
case strings.HasPrefix(attr.Key, "t-att-"):
newKey := strings.TrimPrefix(attr.Key, "t-att-")
var attrValue string
if attr.Value != "" {
attrValue = fmt.Sprintf("{{ %s }}", escapeXMLEntities(attr.Value))
}
elt.CreateAttr(newKey, attrValue)
elt.RemoveAttr(attr.Key)
case strings.HasPrefix(attr.Key, "t-attf-"):
newKey := strings.TrimPrefix(attr.Key, "t-attf-")
elt.CreateAttr(newKey, escapeXMLEntities(attr.Value))
elt.RemoveAttr(attr.Key)
case attr.Key == "t-att":
return fmt.Errorf("hweb does not manage t-att attributes (%s with value '%s')", attr.Key, attr.Value)
}
}
if err := transpileAttributes(elt.ChildElements()); err != nil {
return err
}
}
return nil
}
// transpileConditionals extracts all elements of elts with t-if, t-elif, t-else
// attributes to set Pongo conditionals instead
func transpileConditionals(elts []*etree.Element) error {
type loopElem struct {
attr etree.Attr
elem *etree.Element
}
var (
loops [][]*loopElem
lastLoop []*loopElem
loopClosed bool
isLoopTag bool
)
for _, elt := range elts {
transpileConditionals(elt.ChildElements())
isLoopTag = false
for _, attr := range elt.Attr {
switch attr.Key {
case "t-if":
if len(lastLoop) > 0 {
loops = append(loops, lastLoop)
}
loopClosed = false
isLoopTag = true
lastLoop = []*loopElem{{
attr: attr,
elem: elt,
}}
case "t-elif":
if len(lastLoop) == 0 {
return errors.New("t-elif found without t-if")
}
isLoopTag = true
lastLoop = append(lastLoop, &loopElem{
attr: attr,
elem: elt,
})
case "t-else":
if len(lastLoop) == 0 {
return errors.New("t-else found without t-if")
}
loopClosed = true
isLoopTag = true
lastLoop = append(lastLoop, &loopElem{
attr: attr,
elem: elt,
})
}
}
if loopClosed || !isLoopTag {
if len(lastLoop) > 0 {
loops = append(loops, lastLoop)
lastLoop = nil
}
loopClosed = true
}
}
if len(lastLoop) > 0 {
loops = append(loops, lastLoop)
}
for _, loop := range loops {
lastTag := loop[len(loop)-1]
endCD := etree.NewCharData("{% endif %}")
lastTag.elem.Parent().InsertChild(xmlutils.NextSibling(lastTag.elem), endCD)
for i := 0; i < len(loop); i++ {
cond := escapeXMLEntities(loop[i].attr.Value)
var dir string
switch loop[i].attr.Key {
case "t-if":
dir = fmt.Sprintf("{%% if %s %%}", cond)
case "t-elif":
dir = fmt.Sprintf("{%% elif %s %%}", cond)
case "t-else":
dir = "{% else %}"
}
replaceTag(loop[i].elem, []string{loop[i].attr.Key}, dir)
}
}
return nil
}
// transpileLoops extracts all elements of elts with t-foreach
// attributes to set Pongo loops instead
func transpileLoops(elts []*etree.Element) error {
for _, elt := range elts {
transpileLoops(elt.ChildElements())
fea := elt.SelectAttr("t-foreach")
if fea == nil {
continue
}
aea := elt.SelectAttr("t-as")
if aea == nil {
return errors.New("t-foreach without t-as")
}
endCD := etree.NewCharData("{% endfor %}")
elt.Parent().InsertChild(xmlutils.NextSibling(elt), endCD)
text := fmt.Sprintf("{%% for %s in %s %%}", aea.Value, fea.Value)
replaceTag(elt, []string{"t-foreach", "t-as"}, text)
}
return nil
}
// transpileVariables modifies all children elements of elt with t-set tags
func transpileVariables(elts []*etree.Element) error {
for _, elt := range elts {
transpileVariables(elt.ChildElements())
fea := elt.SelectAttr("t-set")
if fea == nil {
continue
}
if elt.Tag != "t" {
return errors.New("t-set attribute set on non 't' XML tag")
}
val := elt.SelectAttrValue("t-value", "")
switch {
case val != "":
replaceTag(elt, []string{}, fmt.Sprintf("{%% set %s = %s %%}", fea.Value, escapeXMLEntities(val)))
case len(elt.Child) > 0:
endCD := etree.NewCharData("{% endmacro %}")
elt.Parent().InsertChild(xmlutils.NextSibling(elt), endCD)
text := fmt.Sprintf("{%% macro %s() %%}", fea.Value)
replaceTag(elt, []string{}, text)
default:
return errors.New("t-set without t-value nor body")
}
}
return nil
}
// transpileCalls modifies all given elements with t-call attributes
// to set Pongo include directive instead
func transpileCalls(elts []*etree.Element) error {
for _, elt := range elts {
transpileCalls(elt.ChildElements())
fea := elt.SelectAttr("t-call")
if fea == nil {
continue
}
if elt.Tag != "t" {
return errors.New("t-call attribute set on non 't' XML tag")
}
beginWithCD := etree.NewCharData("{% with _0 = null %}")
elt.Parent().InsertChild(elt, beginWithCD)
vars := make(map[string]string)
for _, child := range elt.ChildElements() {
if child.SelectAttr("t-set") != nil {
if child.SelectAttr("t-value") != nil {
vars[child.SelectAttrValue("t-set", "")] = child.SelectAttrValue("t-value", "")
elt.RemoveChild(child)
continue
}
extractToParent(child)
}
}
var withs []string
for k, v := range vars {
withs = append(withs, fmt.Sprintf("%s = %s", k, v))
}
with := strings.Join(withs, " ")
if with != "" {
with = "with " + with
}
// We set a __hexya_template_name variable in order to lazy load the template (avoids infinite recursion)
endWithCD := etree.NewCharData(fmt.Sprintf("{%% set __hexya_template_name = \"%s\" %%}{%% include __hexya_template_name %s %%}\n{%% endwith %%}", fea.Value, with))
elt.Parent().InsertChild(xmlutils.NextSibling(elt), endWithCD)
endCD := etree.NewCharData("{% endmacro %}")
elt.Parent().InsertChild(xmlutils.NextSibling(elt), endCD)
replaceTag(elt, []string{}, "{% macro _0() %}")
}
return nil
}
// transpileSmartFields handles t-field attributes
func transpileSmartFields(elts []*etree.Element) {
}