Skip to content


Subversion checkout URL

You can clone with
Download ZIP
364 lines (305 sloc) 11.5 KB
coffee = require 'coffee-script'
{uglify, parser} = require 'uglify-js'
coffeecup = null
# Call this from the main script so that the compiler module can have access to
# coffeecup exports (node does not allow circular imports).
exports.setup = (cc) ->
coffeecup = cc
skeleton = '''
var __cc = {
buffer: ''
var text = function(txt) {
if (typeof txt === 'string' || txt instanceof String) {
__cc.buffer += txt;
} else if (typeof txt === 'number' || txt instanceof Number) {
__cc.buffer += txt.toString();
var h = function(txt) {
var escaped;
if (typeof txt === 'string' || txt instanceof String) {
escaped = txt.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
} else {
escaped = txt;
return escaped;
var cede = function(f) {
var temp_buffer = '';
var old_buffer = __cc.buffer;
__cc.buffer = temp_buffer;
temp_buffer = __cc.buffer;
__cc.buffer = old_buffer;
return temp_buffer;
call_bound_func = (func) ->
# function(){ <func> }.call(data)
return ['call', ['dot', func, 'call'],
[['name', 'data']]]
# Represents compiled javascript code to be written to the template function.
class Code
constructor: (parent) ->
@parent = parent
@nodes = []
@line = ''
# Returns the ast node for `text(<arg>);`
call: (arg) ->
return ['stat', ['call', ['name', 'text'], [arg]]]
# Add `str` to the current line to be written
append: (str) ->
if @block?
@block.append str
@line += str
# Flush the buffered line to the array of nodes
flush: ->
if @block?
@merge_text ['string', @line]
@line = ''
# Wrap subsequent calls to `text()` in an if block
open_if: (condition) ->
if @block?
@block.open_if condition
@block = new Code()
@block.condition = condition
# Close an if block
close_if: ->
if @block.block?
@nodes.push ['if', @block.condition, ['block', @block.nodes]]
delete @block
# Wrap an ast node in a call to `text()` and add it to the array of nodes
push: (node) ->
if @block?
@block.push node
@merge_text node
# Merge text() calls on the nodes
merge_text: (arg) ->
# Split up string concatenation inside text(), it slows down the template
if arg[0] is 'binary' and arg[1] is '+'
@merge_text arg[2]
arg = arg[3]
# Try to merge strings with previous text() calls
if l = @nodes.length
prev = @nodes[l-1]
# Test if previous node is a call to text()
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'
oldArg = prev[1][2][0]
# Test if the previous and current calls are static strings - if so, combine
ok = ['string', 'num']
if oldArg[0] in ok and arg[0] in ok
prev[1][2][0] = [ 'string', oldArg[1] + arg[1] ]
# We can't combine - just add a call to text()
@nodes.push @call arg
# If the parent statement ends with a semicolon and is not an argument
# to a function, return the statements as separate nodes. Otherwise wrap them
# in an anonymous function bound to the `data` object.
get_nodes: ->
if @parent[0] is 'stat'
return ['splice', @nodes]
return call_bound_func([
null # Anonymous function
[] # Takes no arguments
exports.compile = (source, hardcoded_locals, options) ->
escape = (node) ->
if options.autoescape
# h(<node>)
return ['call', ['name', 'h'], [node]]
return node
ast = parser.parse hardcoded_locals + "(#{source}).call(data);"
w = uglify.ast_walker()
ast = w.with_walkers
call: (expr, args) ->
name = expr[1]
if name is 'doctype'
code = new Code w.parent()
if args.length > 0
doctype = args[0][1].toString()
if doctype of coffeecup.doctypes
code.append coffeecup.doctypes[doctype]
throw new Error 'Invalid doctype'
code.append coffeecup.doctypes.default
return code.get_nodes()
else if name is 'comment'
comment = args[0]
code = new Code w.parent()
if comment[0] is 'string'
code.append "<!--#{comment[1]}-->"
code.append '<!--'
code.push escape comment
code.append '-->'
return code.get_nodes()
else if name is 'ie'
[condition, contents] = args
code = new Code w.parent()
if condition[0] is 'string'
code.append "<!--[if #{condition[1]}]>"
code.append '<!--[if '
code.push escape condition
code.append ']>'
code.push call_bound_func(w.walk contents)
code.append '<![endif]-->'
return code.get_nodes()
else if name in coffeecup.tags or name in ['tag', 'coffeescript']
if name is 'tag'
name = args.shift()[1]
# Compile coffeescript strings to js
if name is 'coffeescript'
name = 'script'
for arg in args
# Dynamically generated coffeescript not supported
if arg[0] not in ['string', 'object', 'function']
throw new Error 'Invalid argument to coffeescript function'
# Make sure this isn't an id class string, and compile it to js
if arg[0] is 'string' and (args.length is 1 or arg isnt args[0])
arg[1] = coffee.compile arg[1], bare: yes
code = new Code w.parent()
code.append "<#{name}"
# Iterate over the arguments to the tag function and build the tag html
# as calls to the `text()` function.
for arg in args
switch arg[0]
when 'function'
# If this is a `<script>` tag, stringify the function
if name is 'script'
func = uglify.gen_code arg,
beautify: true
indent_level: 2
contents = ['string', "#{func}.call(this);"]
# Otherwise recursively check for tag functions and inject the
# result as a bound function call, escaping return values if necessary
func = w.walk arg
# Escape return values unless they are hardcoded strings
for node, idx in func[3]
if node[0] is 'return' and node[1]? and node[1][0] != 'string'
func[3][idx][1] = escape node[1]
contents = call_bound_func(func)
when 'object'
render_attrs = (obj, prefix = '') ->
for attr in obj
key = attr[0]
value = attr[1]
# `true` is rendered as `selected="selected"`.
if value[0] is 'name' and value[1] is 'true'
code.append " #{key}=\"#{key}\""
# Do not render boolean false values
else if value[0] is 'name' and value[1] in ['undefined', 'null', 'false']
# Wrap variables in a conditional block to make sure they are set
else if value[0] in ['name', 'dot']
varname = uglify.gen_code value
# Here we write the `if` condition in js and parse it, as
# writing the nodes manually is tedious and hard to read
condition = "typeof #{varname} !== 'undefined' && #{varname} !== null && #{varname} !== false"
code.open_if parser.parse(condition)[1][0][1] # Strip 'toplevel' and 'stat' labels
code.append " #{prefix + key}=\""
code.push escape value
code.append '"'
# If `value` is a simple string, include it in the same call to
# `text` as the tag
else if value[0] is 'string'
code.append " #{prefix + key}=\"#{value[1]}\""
# Functions are prerendered as text
else if value[0] is 'function'
func = uglify.gen_code(value).replace(/"/g, '&quot;')
code.append " #{prefix + key}=\"#{func}.call(this);\""
# Prefixed attribute
else if value[0] is 'object'
# `data: {icon: 'foo'}` is rendered as `data-icon="foo"`.
render_attrs value[1], prefix + key + '-'
code.append " #{prefix + key}=\""
code.push escape value
code.append '"'
render_attrs arg[1]
when 'string'
# id class string: `"#id.class1.class2"`. Note that this compiler
# only supports hardcoded string values: if you need to determine
# this tag's id or class dynamically, pass the value in an object
# e.g. `div id: @getId(), class: getClasses()`
if args.length > 1 and arg is args[0]
classes = []
for i in arg[1].split '.'
if '#' in i
id = i.replace '#', ''
classes.push i unless i is ''
code.append " id=\"#{id}\"" if id
if classes.length > 0
code.append " class=\"#{classes.join ' '}\""
# Hardcoded string, escape and render it.
arg[1] = arg[1].replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
contents = arg
# A concatenated string e.g. `"id-" + @id`
when 'binary'
# Traverse the ast nodes, selectively escaping anything other
# than hardcoded strings and calls to `cede`.
escape_all = (node) ->
switch node[0]
when 'binary'
node[2] = escape_all node[2]
node[3] = escape_all node[3]
return node
when 'string'
return node
when 'call'
if node[1][0] is 'name' and node[1][1] is 'cede'
return node
return escape node
return escape node
contents = escape_all w.walk arg
# For everything else, put into the template function as is. Note
# that the `text()` function in the template skeleton will only
# output strings and numbers.
contents = escape w.walk arg
if name in coffeecup.self_closing
code.append ' />'
code.append '>'
code.push contents if contents?
if not (name in coffeecup.self_closing)
code.append "</#{name}>"
return code.get_nodes()
# Return the node as-is if this is not a call to a tag function
return null
, ->
return w.walk ast
compiled = uglify.gen_code ast,
beautify: true
indent_level: 2
# Main function assembly.
if options.locals
compiled = "with(data.locals){#{compiled}}"
code = skeleton + compiled + "return __cc.buffer;"
return new Function 'data', code
Jump to Line
Something went wrong with that request. Please try again.