Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 381 lines (310 sloc) 13.123 kB
961c359 @lharding Initial commit.
lharding authored
1 import sys
2 import os
3 import re
4
5 #Copyright (c) 2012 Yahoo! Inc. All rights reserved.
6 #Copyrights licensed under the MIT License. See the accompanying LICENSE file
7 #for terms.
8
9 # define command line options
10 from optparse import OptionParser
11
12 parser = OptionParser()
13 parser.add_option("-f", "--f", dest="filename", help="The mustache template to process")
14 parser.add_option("-r", "--rollup", dest="rollup", action="store_true", help="Rollup this template and it's dependencies into a single file.")
15 parser.add_option("-o", "--output", dest="output", help="The output file of this generated template")
16 parser.add_option("-b", "--basepath", dest="basepath", help="The base path of the mustache directory")
17 parser.add_option("-d", "--baseoutpath", dest="baseoutpath", help="Directory to place output in. Overridden by -o, but not for dependencies.")
18 parser.add_option("-t", "--type", dest="type", help="Whether to output Javascript or PHP")
19
20 (options, args) = parser.parse_args()
21
22 def js_stomp_filename(fname):
23 return re.sub(r'[^a-zA-Z0-9]', '_', fname)
24
25 def jsrepl(matchobj):
26 res = matchobj.group(0)
27 if res.isspace():
28 return ' '
29 else:
30 return '\\' + res
31
32 def js_escape(str):
33 smashspaced = re.sub(r'\s+', ' ', str)
34 return '"'+re.sub(r'([\'"])', jsrepl, smashspaced)+'"'
35
36 # This is used to concat all simple tokens and strings into one .push() statement
37 def js_buffer_append_multi(arr):
38 return "ob.push(" + ", ".join(arr) + ");\n"
39
40 #token types
41 CONTENT = 1
42 DEREF = 2
43 END = 3
44 NEGATE = 4
45 INCLUDE = 5
46 CFGSTRING = 7
47 VARIABLE = 8
48
49 #Dependency list for template being compiled
50 deps = {}
51
52 def token(command, arg):
53 return {
54 'command': command,
55 'arg': arg
56 }
57
58 def genTokens(template):
59 negation = None
60 tokens = []
61
62 start = template.find('{{', 0)
63 lastEnd = 0
64
65 ###################
66 #Tokenization pass#
67 ###################
68
69 #PYTHON SUCKS
70 #TODO: some cases of a missing } can cause this to hang.
71 #If your build fails because this script got wedged here,
72 #look for errors in the template it wass processing when it died.
73 while (start) >= 0:
74 tokens.append(token(CONTENT, template[lastEnd:start]))
75
76 start+=2
77 end = template.find('}}', start)
78 directive = template[start:end]
79
80 command = directive[0:1]
81 name = directive[1:].strip()
82
83 #print "DIR "+directive
84 #print "COM "+command
85 #print "NAM "+name
86 #print "NEG "+str(negation)
87
88 if command=='#':
89 tokens.append(token(DEREF, name))
90 elif command=='/':
91 tokens.append(token(END, None))
92 elif command=='>':
93 tokens.append(token(INCLUDE, name))
94 elif command=='^':
95 #print "entering negation"
96 #push stack just to maintin consistency with block end code...
97 tokens.append(token(NEGATE, name))
98 elif command=='=':
99 print "Unsupported delimiter change directive encountered. Terminating..."
100 exit()
101 elif command=='!':
102 #print "COMMENT: " + directive
103 a=1
104 else:
105 if command != '{':
106 #triple brace means unescape, but we don't handle that right now per ymail behavior
107 name = directive
108 else:
109 end += 1 #we will have an extra } closing this directive, so consume it
110
111 if name.find('str_') == 0:
112 tokens.append(token(CFGSTRING, name))
113 else:
114 tokens.append(token(VARIABLE, name))
115
116 lastEnd = end+2
117 start = template.find('{{', end+2)
118
119 tokens.append(token(CONTENT, template[lastEnd:]))
120 tokens.append(token(END, None))
121
122 return tokens
123
124 def compile_template(filename):
125 global deps
126
127 templateStripped = []
128 try:
129 template = open(filename, 'r')
130 #mustache delimiters will run together with code if we don't have at least a space between lines.
131 templateStripped = [line.strip()+"\n" for line in template]
132 #templateStripped = [line for line in template]
133 except:
134 print "Could not open "+filename
135 return ''
136
137 tokens = genTokens("".join(templateStripped))
138
139 ##TODO: make it return local output buffer instead of modifying global one?
140 deps = {}
141 global options
142
143 if options.type and options.type == 'php':
144 return compileTokensPHP(tokens)[0]
145 else:
146 compiled = compileTokensJS(tokens)[0] #this fills in deps
147 MODULE_PREFIX = 'mu_'
148 depStr = "',\n'".join([MODULE_PREFIX+js_stomp_filename(key) for key in deps.keys()])
149 if len(depStr) != 0: depStr = "'"+depStr+"'"
150
151 try:
152 idx = filename.index("mustache/")
153 idx += 9 # length of 'mustache/'
154 except:
155 idx = 0
156
157 fname = js_stomp_filename(filename[idx:][:-3]) #trim off mustache/ dir and .mu extension
158 if options.rollup:
159 fname = fname + "_rollup"
160
161 res = ("/* AUTO-GENERATED FILE. DO NOT EDIT. */\n" +
162 "YUI.add('"+MODULE_PREFIX+fname+"',function(Y){\n" +
163 "Y.namespace('ui.MuTemplates');\n" +
164 "Y.ui.MuTemplates."+fname+" = function(__ctx, __world) {\n" +
165 "var ob = __world.outbuffer,"+
166 "str=__world.strings,"+
167 "handleHash=__world.handleHashsign,\n"+
168 "templates=__world.templates;\n"+
169 compiled + "\n}\n" +
170 "}, '1.0.0', {requires:["+depStr+"]});")
171
172 return res
173
174 #returns compiled (string, number of tokens consumed)
175 def compileTokensJS(tokens):
176 global deps
177 compiled = ''
178 i = 0
179 tempbuffer = []
180
181 while i < len(tokens) and tokens[i]['command'] != END:
182 #print tokens[i]
183
184 command = tokens[i]['command']
185 arg = tokens[i]['arg']
186
187 res = ('', 1)
188
189 if command==DEREF:
190 # Flush out the tempbuffer
191 if (len(tempbuffer) > 0) :
192 compiled += js_buffer_append_multi(tempbuffer)
193 tempbuffer = []
194
195 res = compileTokensJS(tokens[i+1:])
196 res = ("handleHash(function(__ctx, __world) {\n" + res[0] + "\n}, '" + arg + "', __ctx, __world);", res[1]+1 )
197 elif command==INCLUDE:
198 # Flush out the tempbuffer
199 if (len(tempbuffer) > 0) :
200 compiled += js_buffer_append_multi(tempbuffer)
201 tempbuffer = []
202
203 if options.rollup:
204 basePath = options.basepath
205 templateStripped = []
206 try:
207 print "Processing partial: " + basePath + arg + ".mu"
208 template = open(basePath + arg + ".mu", 'rU')
209 templateStripped = [line.strip()+"\n" for line in template]
210 except:
211 print "Could not open "+ basePath + arg + ".mu"
212 res = ('', 1)
213
214 subtokens = genTokens("".join(templateStripped))
215 res = (compileTokensJS(subtokens )[0], 1)
216 else:
217 deps[arg] = arg
218 res = ("templates."+js_stomp_filename(arg)+"(__ctx, __world);\n", 1)
219 elif command==NEGATE:
220 if (len(tempbuffer) > 0) :
221 compiled += js_buffer_append_multi(tempbuffer)
222 tempbuffer = []
223
224 res = compileTokensJS(tokens[i+1:])
225 res = ("if(!__ctx['"+arg+"']) {\n" + res[0] + "}\n", res[1]+1)
226 elif command==CFGSTRING:
227 tempbuffer.append("str('"+arg+"', __ctx, __world)")
228 elif command==VARIABLE:
229 tempbuffer.append("__ctx['"+arg+"']")
230 elif command==CONTENT:
231 if arg != "":
232 tempbuffer.append(js_escape(arg))
233
234 #print res
235
236 compiled += res[0]
237 i+= res[1]
238
239 # Flush out the tempbuffer
240 if (len(tempbuffer) > 0) :
241 compiled += js_buffer_append_multi(tempbuffer)
242 tempbuffer = []
243
244 return (compiled, i+1)
245
246 #returns compiled (string, number of tokens consumed)
247 def compileTokensPHP(tokens):
248 global deps
249 compiled = ''
250 i = 0
251
252 while i < len(tokens) and tokens[i]['command'] != END:
253 #print tokens[i]
254
255 command = tokens[i]['command']
256 arg = tokens[i]['arg']
257
258 res = ('', 0)
259
260 if command==DEREF:
261 res = compileTokensPHP(tokens[i+1:])
262 res = ("<?php $_varname = '"+arg+"'; " +
263 """
264 $_items = array();
265 $_var = $ctx->$_varname;
266 $_should_descend_context = !is_scalar($_var);
267 if($_var) {
268 if(!is_array($_var)) {
269 $_items[] = $_var;
270 }
271 else {
272 $_items = $_var;
273 }
274 }
275
276 $stk[] = $ctx;
277 foreach($_items as $_ctx_item) {
278 if($_should_descend_context) {
279 $ctx = $_ctx_item;
280 }
281 ?>"""
282 + res[0] + "<?php } $ctx = array_pop($stk); ?>", res[1]+1 )
283 elif command==INCLUDE:
284 if options.rollup:
285 basePath = options.basepath
286 try:
287 print "Processing partial: " + basePath + arg + ".mu"
288 template = open(basePath + arg + ".mu", 'rU')
289 templateStripped = [line.strip()+"\n" for line in template]
290
291 subtokens = genTokens("".join(templateStripped))
292 res = (compileTokensPHP(subtokens )[0], 1)
293 except:
294 print "Could not open "+ basePath + arg + ".mu"
295 res = ('', 1)
296 else:
297 #fname = js_stomp_filename(arg)
298 deps[arg] = arg
299 res = ("<?php include($_TEMPLATE_BASE.'"+arg+".inc'); ?>", 1)
300
301 elif command==NEGATE:
302 res = compileTokensPHP(tokens[i+1:])
303 res = ("<?php $_var = '"+arg+"'; if(!isset($ctx->$_var) || empty($ctx->$_var) ) { ?>" + res[0] + "<?php } ?>", res[1]+1)
304 elif command==CFGSTRING:
305 res = ("<?php echo $this->getIString('"+arg+"', $ctx); ?>", 1)
306 elif command==VARIABLE:
307 res = ("<?php $_var = '"+arg+"'; echo $ctx->$_var; ?>", 1)
308 elif command==CONTENT:
309 res = (arg, 1)
310
311 #print res
312
313 compiled += res[0]
314 i+= res[1]
315
316 return (compiled, i+1)
317
318
319 # setup php path
320 basename, extension = os.path.splitext(options.filename)
321
322 sourcedir = "mustache/"
323 if options.basepath:
324 sourcedir = options.basepath
325
326 destdir = ""
327 if options.baseoutpath:
328 destdir = options.baseoutpath
329
330 if options.output:
331 newPath = options.output
332 else:
333 if options.type and options.type == 'php':
334 newPath = basename.replace(sourcedir, destdir +"php_translated/") + ".inc"
335 else:
336 newPath = basename.replace(sourcedir, destdir + "js_translated/") + ".js"
337
338 newPathDir = os.path.dirname(newPath)
339 if not os.path.exists(newPathDir) :
340 os.makedirs(newPathDir)
341
342 if options.rollup:
343 basename, extension = os.path.splitext(newPath)
344 newPath = basename + "_rollup" + extension
345
346 print "Processing "+options.filename+" into "+newPath
347 #print(compile_template(sys.argv[1]))
348
349 f = open(newPath, 'w')
350 f.write(compile_template(options.filename))
351 f.close()
352
353 #if a basepath has been specified, build dependent templates:
354 if options.basepath and options.baseoutpath:
355 print deps
356 depstack = deps
357
358 while len(depstack) > 0:
359 deps = depstack
360 depstack = {}
361
362 for key in deps.keys():
363 basename = options.basepath + key
364 if options.type and options.type == 'php':
365 newPath = basename.replace(options.basepath, options.baseoutpath + "php_translated/") + ".inc"
366 else:
367 newPath = basename.replace(options.basepath, options.baseoutpath + "js_translated/") + ".js"
368
369 newPathDir = os.path.dirname(newPath)
370 if not os.path.exists(newPathDir) :
371 os.makedirs(newPathDir)
372
373 print "+ Processing dependency "+key+" into "+newPath
374 #print(compile_template(sys.argv[1]))
375
376 f = open(newPath, 'w')
377 f.write(compile_template(options.basepath + key + ".mu"))
378 f.close()
379 depstack.update(deps)
380 elif not options.rollup:
381 print "WARNING: not rollup, and no dependencies generated (basepath and baseoutpath must both be specified to generate dependencies)"
Something went wrong with that request. Please try again.