UNPKG

16.2 kBtext/coffeescriptView Raw
1# **CoffeeCup** lets you to write HTML templates in 100% pure
2# [CoffeeScript](http://coffeescript.org).
3#
4# You can run it on [node.js](http://nodejs.org) or the browser, or compile your
5# templates down to self-contained javascript functions, that will take in data
6# and options and return generated HTML on any JS runtime.
7#
8# The concept is directly stolen from the amazing
9# [Markaby](http://markaby.rubyforge.org/) by Tim Fletcher and why the lucky
10# stiff.
11
12if window?
13 coffeecup = window.coffeecup = {}
14 coffee = if CoffeeScript? then CoffeeScript else null
15else
16 coffeecup = exports
17 coffee = require 'coffee-script'
18 compiler = require __dirname + '/compiler'
19 compiler.setup coffeecup
20 fs = require 'fs'
21
22coffeecup.version = '0.3.18'
23
24# Values available to the `doctype` function inside a template.
25# Ex.: `doctype 'strict'`
26coffeecup.doctypes =
27 'default': '<!DOCTYPE html>'
28 '5': '<!DOCTYPE html>'
29 'xml': '<?xml version="1.0" encoding="utf-8" ?>'
30 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
31 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
32 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
33 '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
34 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
35 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
36 'ce': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "ce-html-1.0-transitional.dtd">'
37
38# CoffeeScript-generated JavaScript may contain anyone of these; but when we
39# take a function to string form to manipulate it, and then recreate it through
40# the `Function()` constructor, it loses access to its parent scope and
41# consequently to any helpers it might need. So we need to reintroduce these
42# inside any "rewritten" function.
43coffeescript_helpers = """
44 var __slice = Array.prototype.slice;
45 var __hasProp = Object.prototype.hasOwnProperty;
46 var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
47 var __extends = function(child, parent) {
48 for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
49 function ctor() { this.constructor = child; }
50 ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype;
51 return child; };
52 var __indexOf = Array.prototype.indexOf || function(item) {
53 for (var i = 0, l = this.length; i < l; i++) {
54 if (this[i] === item) return i;
55 } return -1; };
56""".replace /\n/g, ''
57
58# Private HTML element reference.
59# Please mind the gap (1 space at the beginning of each subsequent line).
60elements =
61 # Valid HTML 5 elements requiring a closing tag.
62 # Note: the `var` element is out for obvious reasons, please use `tag 'var'`.
63 regular: 'a abbr address article aside audio b bdi bdo blockquote body button
64 canvas caption cite code colgroup datalist dd del details dfn div dl dt em
65 fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
66 html i iframe ins kbd label legend li map mark menu meter nav noscript object
67 ol optgroup option output p pre progress q rp rt ruby s samp script section
68 select small span strong style sub summary sup table tbody td textarea tfoot
69 th thead time title tr u ul video'
70
71 # Support for SVG 1.1 tags
72 svg: 'a altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion
73 animateTransform circle clipPath color-profile cursor defs desc ellipse
74 feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix
75 feDiffuseLighting feDisplacementMap feDistantLight feFlood feFuncA feFuncB
76 feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology
77 feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence
78 filter font font-face font-face-format font-face-name font-face-src
79 font-face-uri foreignObject g glyph glyphRef hkern image line linearGradient
80 marker mask metadata missing-glyph mpath path pattern polygon polyline
81 radialGradient rect script set stop style svg symbol text textPath
82 title tref tspan use view vkern'
83
84 # Valid self-closing HTML 5 elements.
85 void: 'area base br col command embed hr img input keygen link meta param
86 source track wbr'
87
88 obsolete: 'applet acronym bgsound dir frameset noframes isindex listing
89 nextid noembed plaintext rb strike xmp big blink center font marquee multicol
90 nobr spacer tt'
91
92 obsolete_void: 'basefont frame'
93
94# Create a unique list of element names merging the desired groups.
95merge_elements = (args...) ->
96 result = []
97 for a in args
98 for element in elements[a].split ' '
99 result.push element unless element in result
100 result
101
102# Public/customizable list of possible elements.
103# For each name in this list that is also present in the input template code,
104# a function with the same name will be added to the compiled template.
105coffeecup.tags = merge_elements 'regular', 'obsolete', 'void', 'obsolete_void',
106 'svg'
107
108# Public/customizable list of elements that should be rendered self-closed.
109coffeecup.self_closing = merge_elements 'void', 'obsolete_void'
110
111# This is the basic material from which compiled templates will be formed.
112# It will be manipulated in its string form at the `coffeecup.compile` function
113# to generate the final template function.
114skeleton = (data = {}) ->
115 # Whether to generate formatted HTML with indentation and line breaks, or
116 # just the natural "faux-minified" output.
117 data.format ?= off
118
119 # Whether to autoescape all content or let you handle it on a case by case
120 # basis with the `h` function.
121 data.autoescape ?= off
122
123 # Internal coffeecup stuff.
124 __cc =
125 buffer: []
126
127 esc: (txt) ->
128 if data.autoescape then h(txt) else txt.toString()
129
130 tabs: 0
131
132 repeat: (string, count) -> Array(count + 1).join string
133
134 indent: -> text @repeat(' ', @tabs) if data.format
135
136 # Adapter to keep the builtin tag functions DRY.
137 tag: (name, args) ->
138 combo = [name]
139 combo.push i for i in args
140 tag.apply data, combo
141
142 render_idclass: (str) ->
143 classes = []
144
145 for i, idx in str.split '.' when i isnt ''
146 # look for an id in the first part
147 if idx is 0 and i.indexOf('#') is 0
148 id = i.slice(1)
149 else
150 classes.push i
151
152 text " id=\"#{id}\"" if id
153
154 if classes.length > 0
155 text " class=\""
156 for c in classes
157 text ' ' unless c is classes[0]
158 text c
159 text '"'
160
161 render_attrs: (obj, prefix = '') ->
162 for k, v of obj
163 # `true` is rendered as `selected="selected"`.
164 v = k if typeof v is 'boolean' and v
165
166 # Functions are rendered in an executable form.
167 v = "(#{v}).call(this);" if typeof v is 'function'
168
169 # Prefixed attribute.
170 if typeof v is 'object' and v not instanceof Array
171 # `data: {icon: 'foo'}` is rendered as `data-icon="foo"`.
172 @render_attrs(v, prefix + k + '-')
173 # `undefined`, `false` and `null` result in the attribute not being rendered.
174 else if v or v==0
175 # strings, numbers, arrays and functions are rendered "as is".
176 text " #{prefix + k}=\"#{@esc(v)}\""
177
178 render_contents: (contents, safe) ->
179 safe ?= false
180 switch typeof contents
181 when 'string', 'number', 'boolean'
182 text if safe then contents else @esc(contents)
183 when 'function'
184 text '\n' if data.format
185 @tabs++
186 result = contents.call data
187 if typeof result is 'string'
188 @indent()
189 text if safe then result else @esc(result)
190 text '\n' if data.format
191 @tabs--
192 @indent()
193
194 render_tag: (name, idclass, attrs, inline, contents) ->
195 @indent()
196
197 text "<#{name}"
198 @render_idclass(idclass) if idclass
199 @render_attrs(attrs) if attrs
200
201 text " #{inline}" if inline
202
203 if name in @self_closing
204 text ' />'
205 text '\n' if data.format
206 else
207 text '>'
208
209 @render_contents(contents)
210
211 text "</#{name}>"
212 text '\n' if data.format
213
214 null
215
216 tag = (name, args...) ->
217 for a in args
218 switch typeof a
219 when 'function'
220 contents = a
221 when 'object'
222 attrs = a
223 when 'number', 'boolean'
224 contents = a
225 when 'string'
226 if args.length is 1
227 contents = a
228 else
229 if a is args[0]
230 first = a.charAt(0)
231 if first == '#' || first == '.'
232 idclass = a.substr(0, a.indexOf(' '))
233 inline = a.substr(a.indexOf(' ') + 1)
234 if idclass == ''
235 idclass = inline
236 inline = undefined
237 else
238 inline = a
239 inline = undefined if inline == ''
240 else
241 contents = a
242
243 __cc.render_tag(name, idclass, attrs, inline, contents)
244
245 cede = (f) ->
246 temp_buffer = []
247 old_buffer = __cc.buffer
248 __cc.buffer = temp_buffer
249 f()
250 __cc.buffer = old_buffer
251 temp_buffer.join ''
252
253 h = (txt) ->
254 txt.toString().replace(/&/g, '&amp;')
255 .replace(/</g, '&lt;')
256 .replace(/>/g, '&gt;')
257 .replace(/"/g, '&quot;')
258
259 doctype = (type = 'default') ->
260 text __cc.doctypes[type]
261 text '\n' if data.format
262
263 text = (txt) ->
264 __cc.buffer.push txt.toString()
265 null
266
267 comment = (cmt) ->
268 text "<!--#{cmt}-->"
269 text '\n' if data.format
270
271 coffeescript = (param) ->
272 switch typeof param
273 # `coffeescript -> alert 'hi'` becomes:
274 # `<script>;(function () {return alert('hi');})();</script>`
275 when 'function'
276 script "#{__cc.coffeescript_helpers}(#{param}).call(this);"
277 # `coffeescript "alert 'hi'"` becomes:
278 # `<script type="text/coffeescript">alert 'hi'</script>`
279 when 'string'
280 script type: 'text/coffeescript', -> param
281 # `coffeescript src: 'script.coffee'` becomes:
282 # `<script type="text/coffeescript" src="script.coffee"></script>`
283 when 'object'
284 param.type = 'text/coffeescript'
285 script param
286
287 stylus = (s) ->
288 throw new TemplateError('stylus is not available') unless data.stylus?
289 text '<style>'
290 text '\n' if data.format
291 data.stylus.render s, {compress: not data.format}, (err, css) ->
292 if err then throw err
293 text css
294 text '</style>'
295 text '\n' if data.format
296
297 # Conditional IE comments.
298 ie = (condition, contents) ->
299 __cc.indent()
300
301 text "<!--[if #{condition}]>"
302 __cc.render_contents(contents)
303 text "<![endif]-->"
304 text '\n' if data.format
305
306 null
307
308# Stringify the skeleton and unwrap it from its enclosing `function(){}`, then
309# add the CoffeeScript helpers.
310skeleton = skeleton.toString()
311 .replace(/function\s*\(.*\)\s*\{/, '')
312 .replace(/return null;\s*\}$/, '')
313
314skeleton = coffeescript_helpers + skeleton
315
316# Compiles a template into a standalone JavaScript function.
317coffeecup.compile = (template, options = {}) ->
318 # The template can be provided as either a function or a CoffeeScript string
319 # (in the latter case, the CoffeeScript compiler must be available).
320 if typeof template is 'function' then template = template.toString()
321 else if typeof template is 'string' and coffee?
322 template = coffee.compile template, bare: yes
323 template = "function(){#{template}}"
324
325 # If an object `hardcode` is provided, insert the stringified value
326 # of each variable directly in the function body. This is a less flexible but
327 # faster alternative to the standard method of using `with` (see below).
328 hardcoded_locals = ''
329
330 if options.hardcode
331 for k, v of options.hardcode
332 if typeof v is 'function'
333 # Make sure these functions have access to `data` as `@/this`.
334 hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};"
335 else hardcoded_locals += "var #{k} = #{JSON.stringify v};"
336
337 # If `optimize` is set on the options hash, use uglify-js to parse the
338 # template function's code and optimize it using static analysis.
339 if options.optimize and compiler?
340 return compiler.compile template, hardcoded_locals, options
341
342 # Add a function for each tag this template references. We don't want to have
343 # all hundred-odd tags wasting space in the compiled function.
344 tag_functions = ''
345 tags_used = []
346
347 for t in coffeecup.tags
348 if template.indexOf(t) > -1 or hardcoded_locals.indexOf(t) > -1
349 tags_used.push t
350
351 tag_functions += "var #{tags_used.join ','};"
352 for t in tags_used
353 tag_functions += "#{t} = function(){return __cc.tag('#{t}', arguments);};"
354
355 # Main function assembly.
356 code = tag_functions + hardcoded_locals + skeleton
357
358 code += "__cc.doctypes = #{JSON.stringify coffeecup.doctypes};"
359 code += "__cc.coffeescript_helpers = #{JSON.stringify coffeescript_helpers};"
360 code += "__cc.self_closing = #{JSON.stringify coffeecup.self_closing};"
361
362 # If `locals` is set, wrap the template inside a `with` block. This is the
363 # most flexible but slower approach to specifying local variables.
364 code += 'with(data.locals){' if options.locals
365 code += "(#{template}).call(data);"
366 code += '}' if options.locals
367 code += "return __cc.buffer.join('');"
368
369 new Function('data', code)
370
371cache = {}
372
373# Template in, HTML out. Accepts functions or strings as does `coffeecup.compile`.
374#
375# Accepts an option `cache`, by default `false`. If set to `false` templates will
376# be recompiled each time.
377#
378# `options` is just a convenience parameter to pass options separately from the
379# data, but the two will be merged and passed down to the compiler (which uses
380# `locals` and `hardcode`), and the template (which understands `locals`, `format`
381# and `autoescape`).
382coffeecup.render = (template, data = {}, options = {}) ->
383 data[k] = v for k, v of options
384 data.cache ?= off
385
386 if not window?
387 data.stylus = require 'stylus'
388
389 # Do not optimize templates if the cache is disabled, as it will slow
390 # everything down considerably.
391 if data.optimize and not data.cache then data.optimize = no
392
393 if data.cache and cache[template]? then tpl = cache[template]
394 else if data.cache then tpl = cache[template] = coffeecup.compile(template, data)
395 else tpl = coffeecup.compile(template, data)
396 tpl(data)
397
398unless window?
399 coffeecup.__express = (path, options = {}, fn) ->
400 options.stylus = require 'stylus'
401 if options.optimize and not options.cache then options.optimize = no
402
403 render = (tpl) ->
404 try
405 fn null, tpl(options)
406 catch err
407 fn err
408
409 if options.cache and cache[path]?
410 tpl = cache[path]
411 render(tpl)
412 else
413 fs.readFile path, 'utf8', (err, str) ->
414 if err then return fn err
415 try
416 tpl = coffeecup.compile(str,options)
417 catch err
418 return fn err
419
420 if options.cache then cache[path] = tpl
421 render(tpl)
422
423 coffeecup.adapters =
424 # Legacy adapters for when coffeecup expected data in the `context` attribute.
425 simple: coffeecup.render
426 meryl: coffeecup.render
427
428 express:
429 TemplateError: class extends Error
430 constructor: (@message) ->
431 Error.call this, @message
432 Error.captureStackTrace this, arguments.callee
433 name: 'TemplateError'
434
435 compile: (template, data) ->
436 # Allows `partial 'foo'` instead of `text @partial 'foo'`.
437 data.hardcode ?= {}
438 data.hardcode.partial = ->
439 text @partial.apply @, arguments
440
441 TemplateError = @TemplateError
442 try tpl = coffeecup.compile(template, data)
443 catch e then throw new TemplateError "Error compiling #{data.filename}: #{e.message}"
444
445 return ->
446 try tpl arguments...
447 catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"