UNPKG

15.4 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.12'
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 in str.split '.'
145 if '#' in i
146 id = i.replace '#', ''
147 else
148 classes.push i unless i is ''
149
150 text " id=\"#{id}\"" if id
151
152 if classes.length > 0
153 text " class=\""
154 for c in classes
155 text ' ' unless c is classes[0]
156 text c
157 text '"'
158
159 render_attrs: (obj, prefix = '') ->
160 for k, v of obj
161 # `true` is rendered as `selected="selected"`.
162 v = k if typeof v is 'boolean' and v
163
164 # Functions are rendered in an executable form.
165 v = "(#{v}).call(this);" if typeof v is 'function'
166
167 # Prefixed attribute.
168 if typeof v is 'object' and v not instanceof Array
169 # `data: {icon: 'foo'}` is rendered as `data-icon="foo"`.
170 @render_attrs(v, prefix + k + '-')
171 # `undefined`, `false` and `null` result in the attribute not being rendered.
172 else if v
173 # strings, numbers, arrays and functions are rendered "as is".
174 text " #{prefix + k}=\"#{@esc(v)}\""
175
176 render_contents: (contents, safe) ->
177 safe ?= false
178 switch typeof contents
179 when 'string', 'number', 'boolean'
180 text if safe then contents else @esc(contents)
181 when 'function'
182 text '\n' if data.format
183 @tabs++
184 result = contents.call data
185 if typeof result is 'string'
186 @indent()
187 text if safe then result else @esc(result)
188 text '\n' if data.format
189 @tabs--
190 @indent()
191
192 render_tag: (name, idclass, attrs, inline, contents) ->
193 @indent()
194
195 text "<#{name}"
196 @render_idclass(idclass) if idclass
197 @render_attrs(attrs) if attrs
198
199 text " #{inline}" if inline
200
201 if name in @self_closing
202 text ' />'
203 text '\n' if data.format
204 else
205 text '>'
206
207 @render_contents(contents)
208
209 text "</#{name}>"
210 text '\n' if data.format
211
212 null
213
214 tag = (name, args...) ->
215 for a in args
216 switch typeof a
217 when 'function'
218 contents = a
219 when 'object'
220 attrs = a
221 when 'number', 'boolean'
222 contents = a
223 when 'string'
224 if args.length is 1
225 contents = a
226 else
227 if a is args[0]
228 first = a.charAt(0)
229 if first == '#' || first == '.'
230 idclass = a.substr(0, a.indexOf(' '))
231 inline = a.substr(a.indexOf(' ') + 1)
232 if idclass == ''
233 idclass = inline
234 inline = undefined
235 else
236 inline = a
237 inline = undefined if inline == ''
238 else
239 contents = a
240
241 __cc.render_tag(name, idclass, attrs, inline, contents)
242
243 cede = (f) ->
244 temp_buffer = []
245 old_buffer = __cc.buffer
246 __cc.buffer = temp_buffer
247 f()
248 __cc.buffer = old_buffer
249 temp_buffer.join ''
250
251 h = (txt) ->
252 txt.toString().replace(/&/g, '&amp;')
253 .replace(/</g, '&lt;')
254 .replace(/>/g, '&gt;')
255 .replace(/"/g, '&quot;')
256
257 doctype = (type = 'default') ->
258 text __cc.doctypes[type]
259 text '\n' if data.format
260
261 text = (txt) ->
262 __cc.buffer.push txt.toString()
263 null
264
265 comment = (cmt) ->
266 text "<!--#{cmt}-->"
267 text '\n' if data.format
268
269 coffeescript = (param) ->
270 switch typeof param
271 # `coffeescript -> alert 'hi'` becomes:
272 # `<script>;(function () {return alert('hi');})();</script>`
273 when 'function'
274 script "#{__cc.coffeescript_helpers}(#{param}).call(this);"
275 # `coffeescript "alert 'hi'"` becomes:
276 # `<script type="text/coffeescript">alert 'hi'</script>`
277 when 'string'
278 script type: 'text/coffeescript', -> param
279 # `coffeescript src: 'script.coffee'` becomes:
280 # `<script type="text/coffeescript" src="script.coffee"></script>`
281 when 'object'
282 param.type = 'text/coffeescript'
283 script param
284
285 stylus = (s) ->
286 text '<style>'
287 text '\n' if data.format
288 data.stylus.render s, {compress: not data.format}, (err, css) ->
289 if err then throw err
290 text css
291 text '</style>'
292 text '\n' if data.format
293
294 # Conditional IE comments.
295 ie = (condition, contents) ->
296 __cc.indent()
297
298 text "<!--[if #{condition}]>"
299 __cc.render_contents(contents)
300 text "<![endif]-->"
301 text '\n' if data.format
302
303 null
304
305# Stringify the skeleton and unwrap it from its enclosing `function(){}`, then
306# add the CoffeeScript helpers.
307skeleton = skeleton.toString()
308 .replace(/function\s*\(.*\)\s*\{/, '')
309 .replace(/return null;\s*\}$/, '')
310
311skeleton = coffeescript_helpers + skeleton
312
313# Compiles a template into a standalone JavaScript function.
314coffeecup.compile = (template, options = {}) ->
315 # The template can be provided as either a function or a CoffeeScript string
316 # (in the latter case, the CoffeeScript compiler must be available).
317 if typeof template is 'function' then template = template.toString()
318 else if typeof template is 'string' and coffee?
319 template = coffee.compile template, bare: yes
320 template = "function(){#{template}}"
321
322 # If an object `hardcode` is provided, insert the stringified value
323 # of each variable directly in the function body. This is a less flexible but
324 # faster alternative to the standard method of using `with` (see below).
325 hardcoded_locals = ''
326
327 if options.hardcode
328 for k, v of options.hardcode
329 if typeof v is 'function'
330 # Make sure these functions have access to `data` as `@/this`.
331 hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};"
332 else hardcoded_locals += "var #{k} = #{JSON.stringify v};"
333
334 # If `optimize` is set on the options hash, use uglify-js to parse the
335 # template function's code and optimize it using static analysis.
336 if options.optimize and compiler?
337 return compiler.compile template, hardcoded_locals, options
338
339 # Add a function for each tag this template references. We don't want to have
340 # all hundred-odd tags wasting space in the compiled function.
341 tag_functions = ''
342 tags_used = []
343
344 for t in coffeecup.tags
345 if template.indexOf(t) > -1 or hardcoded_locals.indexOf(t) > -1
346 tags_used.push t
347
348 tag_functions += "var #{tags_used.join ','};"
349 for t in tags_used
350 tag_functions += "#{t} = function(){return __cc.tag('#{t}', arguments);};"
351
352 # Main function assembly.
353 code = tag_functions + hardcoded_locals + skeleton
354
355 code += "__cc.doctypes = #{JSON.stringify coffeecup.doctypes};"
356 code += "__cc.coffeescript_helpers = #{JSON.stringify coffeescript_helpers};"
357 code += "__cc.self_closing = #{JSON.stringify coffeecup.self_closing};"
358
359 # If `locals` is set, wrap the template inside a `with` block. This is the
360 # most flexible but slower approach to specifying local variables.
361 code += 'with(data.locals){' if options.locals
362 code += "(#{template}).call(data);"
363 code += '}' if options.locals
364 code += "return __cc.buffer.join('');"
365
366 new Function('data', code)
367
368cache = {}
369
370# Template in, HTML out. Accepts functions or strings as does `coffeecup.compile`.
371#
372# Accepts an option `cache`, by default `false`. If set to `false` templates will
373# be recompiled each time.
374#
375# `options` is just a convenience parameter to pass options separately from the
376# data, but the two will be merged and passed down to the compiler (which uses
377# `locals` and `hardcode`), and the template (which understands `locals`, `format`
378# and `autoescape`).
379coffeecup.render = (template, data = {}, options = {}) ->
380 data[k] = v for k, v of options
381 data.cache ?= off
382 data.stylus = require 'stylus'
383
384 # Do not optimize templates if the cache is disabled, as it will slow
385 # everything down considerably.
386 if data.optimize and not data.cache then data.optimize = no
387
388 if data.cache and cache[template]? then tpl = cache[template]
389 else if data.cache then tpl = cache[template] = coffeecup.compile(template, data)
390 else tpl = coffeecup.compile(template, data)
391 tpl(data)
392
393unless window?
394 coffeecup.adapters =
395 # Legacy adapters for when coffeecup expected data in the `context` attribute.
396 simple: coffeecup.render
397 meryl: coffeecup.render
398
399 express:
400 TemplateError: class extends Error
401 constructor: (@message) ->
402 Error.call this, @message
403 Error.captureStackTrace this, arguments.callee
404 name: 'TemplateError'
405
406 compile: (template, data) ->
407 # Allows `partial 'foo'` instead of `text @partial 'foo'`.
408 data.hardcode ?= {}
409 data.hardcode.partial = ->
410 text @partial.apply @, arguments
411
412 TemplateError = @TemplateError
413 try tpl = coffeecup.compile(template, data)
414 catch e then throw new TemplateError "Error compiling #{data.filename}: #{e.message}"
415
416 return ->
417 try tpl arguments...
418 catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"