UNPKG

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