UNPKG

11.7 kBtext/coffeescriptView Raw
1coffee = require 'coffee-script'
2{uglify, parser} = require 'uglify-js'
3coffeecup = null
4
5# Call this from the main script so that the compiler module can have access to
6# coffeecup exports (node does not allow circular imports).
7exports.setup = (cc) ->
8 coffeecup = cc
9
10skeleton = '''
11 var __cc = {
12 buffer: ''
13 };
14 var text = function(txt) {
15 if (typeof txt === 'string' || txt instanceof String) {
16 __cc.buffer += txt;
17 } else if (typeof txt === 'number' || txt instanceof Number) {
18 __cc.buffer += txt.toString();
19 }
20 };
21 var h = function(txt) {
22 var escaped;
23 if (typeof txt === 'string' || txt instanceof String) {
24 escaped = txt.replace(/&/g, '&')
25 .replace(/</g, '&lt;')
26 .replace(/>/g, '&gt;')
27 .replace(/"/g, '&quot;');
28 } else {
29 escaped = txt;
30 }
31 return escaped;
32 };
33 var cede = function(f) {
34 var temp_buffer = '';
35 var old_buffer = __cc.buffer;
36 __cc.buffer = temp_buffer;
37 f();
38 temp_buffer = __cc.buffer;
39 __cc.buffer = old_buffer;
40 return temp_buffer;
41 };
42
43'''
44
45call_bound_func = (func) ->
46 # function(){ <func> }.call(data)
47 return ['call', ['dot', func, 'call'],
48 [['name', 'data']]]
49
50# Represents compiled javascript code to be written to the template function.
51class Code
52 constructor: (parent) ->
53 @parent = parent
54 @nodes = []
55 @line = ''
56
57 # Returns the ast node for `text(<arg>);`
58 call: (arg) ->
59 return ['stat', ['call', ['name', 'text'], [arg]]]
60
61 # Add `str` to the current line to be written
62 append: (str) ->
63 if @block?
64 @block.append str
65 else
66 @line += str
67
68 # Flush the buffered line to the array of nodes
69 flush: ->
70 if @block?
71 @block.flush()
72 else
73 @merge_text ['string', @line]
74 @line = ''
75
76 # Wrap subsequent calls to `text()` in an if block
77 open_if: (condition) ->
78 @flush()
79 if @block?
80 @block.open_if condition
81 else
82 @block = new Code()
83 @block.condition = condition
84
85 # Close an if block
86 close_if: ->
87 @flush()
88 if @block.block?
89 @block.close_if()
90 else
91 @nodes.push ['if', @block.condition, ['block', @block.nodes]]
92 delete @block
93
94 # Wrap an ast node in a call to `text()` and add it to the array of nodes
95 push: (node) ->
96 @flush()
97 if @block?
98 @block.push node
99 else
100 @merge_text node
101
102 # Merge text() calls on the nodes
103 merge_text: (arg) ->
104 # Split up string concatenation inside text(), it slows down the template
105 if arg[0] is 'binary' and arg[1] is '+'
106 @merge_text arg[2]
107 arg = arg[3]
108
109 # Try to merge strings with previous text() calls
110 if l = @nodes.length
111 prev = @nodes[l-1]
112 # Test if previous node is a call to text()
113 if prev[0] is 'stat' and prev[1][0] is 'call' and prev[1][1][0] is 'name' and prev[1][1][1] is 'text'
114 oldArg = prev[1][2][0]
115 # Test if the previous and current calls are static strings - if so, combine
116 ok = ['string', 'num']
117 if oldArg[0] in ok and arg[0] in ok
118 prev[1][2][0] = [ 'string', oldArg[1] + arg[1] ]
119 return
120
121 # We can't combine - just add a call to text()
122 @nodes.push @call arg
123
124
125 # If the parent statement ends with a semicolon and is not an argument
126 # to a function, return the statements as separate nodes. Otherwise wrap them
127 # in an anonymous function bound to the `data` object.
128 get_nodes: ->
129 @flush()
130
131 if @parent[0] is 'stat'
132 return ['splice', @nodes]
133
134 return call_bound_func([
135 'function'
136 null # Anonymous function
137 [] # Takes no arguments
138 @nodes
139 ])
140
141
142exports.compile = (source, hardcoded_locals, options) ->
143
144 escape = (node) ->
145 if options.autoescape
146 # h(<node>)
147 return ['call', ['name', 'h'], [node]]
148 return node
149
150 ast = parser.parse hardcoded_locals + "(#{source}).call(data);"
151 w = uglify.ast_walker()
152 ast = w.with_walkers
153 call: (expr, args) ->
154 name = expr[1]
155
156 if name is 'doctype'
157 code = new Code w.parent()
158 if args.length > 0
159 doctype = args[0][1].toString()
160 if doctype of coffeecup.doctypes
161 code.append coffeecup.doctypes[doctype]
162 else
163 throw new Error 'Invalid doctype'
164 else
165 code.append coffeecup.doctypes.default
166 return code.get_nodes()
167
168 else if name is 'comment'
169 comment = args[0]
170 code = new Code w.parent()
171 if comment[0] is 'string'
172 code.append "<!--#{comment[1]}-->"
173 else
174 code.append '<!--'
175 code.push escape comment
176 code.append '-->'
177 return code.get_nodes()
178
179 else if name is 'ie'
180 [condition, contents] = args
181 code = new Code w.parent()
182 if condition[0] is 'string'
183 code.append "<!--[if #{condition[1]}]>"
184 else
185 code.append '<!--[if '
186 code.push escape condition
187 code.append ']>'
188 code.push call_bound_func(w.walk contents)
189 code.append '<![endif]-->'
190 return code.get_nodes()
191
192 else if name in coffeecup.tags or name in ['tag', 'coffeescript']
193 if name is 'tag'
194 name = args.shift()[1]
195
196 # Compile coffeescript strings to js
197 if name is 'coffeescript'
198 name = 'script'
199 for arg in args
200 # Dynamically generated coffeescript not supported
201 if arg[0] not in ['string', 'object', 'function']
202 throw new Error 'Invalid argument to coffeescript function'
203 # Make sure this isn't an id class string, and compile it to js
204 if arg[0] is 'string' and (args.length is 1 or arg isnt args[0])
205 arg[1] = coffee.compile arg[1], bare: yes
206
207 code = new Code w.parent()
208 code.append "<#{name}"
209
210 # Iterate over the arguments to the tag function and build the tag html
211 # as calls to the `text()` function.
212 for arg in args
213 switch arg[0]
214
215 when 'function'
216 # If this is a `<script>` tag, stringify the function
217 if name is 'script'
218 func = uglify.gen_code arg,
219 beautify: true
220 indent_level: 2
221 contents = ['string', "#{func}.call(this);"]
222 # Otherwise recursively check for tag functions and inject the
223 # result as a bound function call, escaping return values if necessary
224 else
225 func = w.walk arg
226
227 # Escape return values unless they are hardcoded strings
228 for node, idx in func[3]
229 if node[0] is 'return' and node[1]? and node[1][0] != 'string'
230 func[3][idx][1] = escape node[1]
231
232 contents = call_bound_func(func)
233
234 when 'object'
235 render_attrs = (obj, prefix = '') ->
236 for attr in obj
237 key = attr[0]
238 value = attr[1]
239
240 # `true` is rendered as `selected="selected"`.
241 if value[0] is 'name' and value[1] is 'true'
242 code.append " #{key}=\"#{key}\""
243
244 # Do not render boolean false values
245 else if value[0] is 'name' and value[1] in ['undefined', 'null', 'false']
246 continue
247
248 # Wrap variables in a conditional block to make sure they are set
249 else if value[0] in ['name', 'dot']
250 varname = uglify.gen_code value
251 # Here we write the `if` condition in js and parse it, as
252 # writing the nodes manually is tedious and hard to read
253 condition = "typeof #{varname} !== 'undefined' && #{varname} !== null && #{varname} !== false"
254 code.open_if parser.parse(condition)[1][0][1] # Strip 'toplevel' and 'stat' labels
255 code.append " #{prefix + key}=\""
256 code.push escape value
257 code.append '"'
258 code.close_if()
259
260 # If `value` is a simple string, include it in the same call to
261 # `text` as the tag
262 else if value[0] is 'string'
263 code.append " #{prefix + key}=\"#{value[1]}\""
264
265 # Functions are prerendered as text
266 else if value[0] is 'function'
267 func = uglify.gen_code(value).replace(/"/g, '&quot;')
268 code.append " #{prefix + key}=\"#{func}.call(this);\""
269
270 # Prefixed attribute
271 else if value[0] is 'object'
272 # `data: {icon: 'foo'}` is rendered as `data-icon="foo"`.
273 render_attrs value[1], prefix + key + '-'
274
275 else
276 code.append " #{prefix + key}=\""
277 code.push escape value
278 code.append '"'
279
280 render_attrs arg[1]
281
282 when 'string'
283 # id class string: `"#id.class1.class2"`. Note that this compiler
284 # only supports hardcoded string values: if you need to determine
285 # this tag's id or class dynamically, pass the value in an object
286 # e.g. `div id: @getId(), class: getClasses()`
287 if args.length > 1 and arg is args[0]
288 classes = []
289
290 for i in arg[1].split '.'
291 if '#' in i
292 id = i.replace '#', ''
293 else
294 classes.push i unless i is ''
295
296 code.append " id=\"#{id}\"" if id
297
298 if classes.length > 0
299 code.append " class=\"#{classes.join ' '}\""
300
301 # Hardcoded string, escape and render it.
302 else
303 arg[1] = arg[1].replace(/&/g, '&amp;')
304 .replace(/</g, '&lt;')
305 .replace(/>/g, '&gt;')
306 .replace(/"/g, '&quot;')
307 contents = arg
308
309 # A concatenated string e.g. `"id-" + @id`
310 when 'binary'
311
312 # Traverse the ast nodes, selectively escaping anything other
313 # than hardcoded strings and calls to `cede`.
314 escape_all = (node) ->
315 switch node[0]
316 when 'binary'
317 node[2] = escape_all node[2]
318 node[3] = escape_all node[3]
319 return node
320 when 'string'
321 return node
322 when 'call'
323 if node[1][0] is 'name' and node[1][1] is 'cede'
324 return node
325 return escape node
326 else
327 return escape node
328
329 contents = escape_all w.walk arg
330
331 # For everything else, put into the template function as is. Note
332 # that the `text()` function in the template skeleton will only
333 # output strings and numbers.
334 else
335 contents = escape w.walk arg
336
337 if name in coffeecup.self_closing
338 code.append ' />'
339 else
340 code.append '>'
341
342 code.push contents if contents?
343 if not (name in coffeecup.self_closing)
344 code.append "</#{name}>"
345
346 return code.get_nodes()
347
348 # Return the node as-is if this is not a call to a tag function
349 return null
350 , ->
351 return w.walk ast
352
353 compiled = uglify.gen_code ast,
354 beautify: true
355 indent_level: 2
356
357 # Main function assembly.
358 if options.locals
359 compiled = "with(data.locals){#{compiled}}"
360 code = skeleton + compiled + "return __cc.buffer;"
361
362 return new Function 'data', code
363