Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Optimize compiled templates #87

Open
wants to merge 59 commits into from

4 participants

@omarkhan

I have been experimenting with optimizing coffeekup templates using UglifyJS to analyze the template source and prerender the html. The idea is to turn this:

function() {
  //
  // Lots of boilerplate code...
  //
  html(function() {
    head(function() {
      return title(this.title);
    });
    return body(function() {
      h1(this.title);
      return p('Super fast templates');
    });
  });}).call(data);return __ck.buffer.join('');
}

into this:

function() {
  //
  // Much less boilerplate
  //
  ((function() {
    text("<!DOCTYPE html>");
    text("<html>");
    text(function() {
      text("<head>");
      text(function() {
        return function() {
          text("<title>");
          text(this.title);
          text("</title>");
        }.call(data);
      }.call(data));
      text("</head>");
      return function() {
        text("<body>");
        text(function() {
          text("<h1>");
          text(this.title);
          text("</h1>");
          return function() {
            text("<p>");
            text("Super fast templates");
            text("</p>");
          }.call(data);
        }.call(data));
        text("</body>");
      }.call(data);
    }.call(data));
    text("</html>");
  })).call(data);return __ck.buffer;
}

The advantages of this approach are shorter template functions (unless the template is very long) thanks to reduced boilerplate, and faster execution owing to much of the work being done at compile time. A further performance improvement is achieved by using string concatenation rather than array join to build the output. Here are my benchmark results, running a compiled template 5000 times on node:

CoffeeKup (precompiled): 263 ms
CoffeeKup (precompiled, optimized): 24 ms
Jade (precompiled): 530 ms
haml-js (precompiled): 89 ms
Eco: 92 ms

See here for browser rendering performance: http://jsperf.com/coffeekup-optimized/2

As far as I can tell, these optimizations give a 10x performance boost on node, a 30x boost in Chrome and a more modest 3x improvement in Firefox.

Usage

template = coffeekup.compile 'h1 @title', optimize: yes
template(title: 'Super fast template')
# '<h1>Super fast template</h1>'

Caveats

As this compiler uses static analysis to optimize your templates, too much complex logic may cause it to fall over. But templates should be logic-free so that shouldn't be a problem, right?

I have aimed for API compatibility with regular coffeekup, but there are a few differences to be aware of:

  • The coffeescript helper function does not add "text/coffeescript" to the <script> tag. I could implement this, but it doesn't strike me as very useful.
  • Output formatting is not implemented (yet). You get a single line of html.
  • Arrays are not rendered directly in the template output. Join them into a string before passing them to the compiled template function.
  • You can't set up a hash of options and then pass it to the tag function, e.g.
attrs =
  name: 'email'
  type: 'text'
input attrs

Do this instead:

input
  name: 'email'
  type: 'text'

I have made some minor modifications to the test suite to account for the above.

omarkhan added some commits
@omarkhan omarkhan Simplified executable ab4a134
@omarkhan omarkhan Added basic optimised compiler c029eba
@omarkhan omarkhan Get exports from main script f22d1b4
@omarkhan omarkhan Removed function wrapper f4becea
@omarkhan omarkhan Added hardcoded locals f8a1739
@omarkhan omarkhan Compiler can handle tag arguments other than functions and objects 0d53c78
@omarkhan omarkhan Wrap compiled code in bound function to preserve scope 6f2f320
@omarkhan omarkhan Unwrap calls to tag functions from function wrapper when not needed 42952c0
@omarkhan omarkhan Fixed function wrapper 61adb49
@omarkhan omarkhan Hardcoded locals also checked for markup tags bf775ed
@omarkhan omarkhan Added doctype cf5fb39
@omarkhan omarkhan Allow for tags without content 53039fd
@omarkhan omarkhan Removed broken jade benchmarks c87c3ee
@omarkhan omarkhan Merge branch 'master' into optimize 362152a
@omarkhan omarkhan Skeleton test() function takes numbers as well as strings cce76e0
@omarkhan omarkhan Code cleanup, comments 5b50c62
@omarkhan omarkhan Added escape function h() to skeleton 3ba8a58
@omarkhan omarkhan Added tag() function 631b630
@omarkhan omarkhan Added comment() function 43ee3fc
@omarkhan omarkhan Don't try to put too much in a single call to text() 8ac5838
@omarkhan omarkhan Refactor e01a404
@omarkhan omarkhan Added support for script and coffeescript functions ecb2c59
@omarkhan omarkhan Comments for clarity 571732c
@omarkhan omarkhan Do not parse id class string for script tags 48700e0
@omarkhan omarkhan Fixed coffeescript tag function 060acd3
@omarkhan omarkhan Added locals option 83f98b1
@omarkhan omarkhan Added ie conditional comment helper 064faff
@omarkhan omarkhan Tag functions can handle prefixed attributes 2d9611f
@omarkhan omarkhan Boolean true in tag attrs rendered as selected="selected" 04cda25
@omarkhan omarkhan Functions can be passed to tag attrs object 000dc5e
@omarkhan omarkhan Added yield function to skeleton 68cefd7
@omarkhan omarkhan Added autoescape option 3b58ad4
@omarkhan omarkhan Do not escape calls to yield 87fc65e
@omarkhan omarkhan Escape tag contents by default 85d1302
@omarkhan omarkhan Escape return values of functions passed as arguments to tag functions 7267231
@omarkhan omarkhan Escape dynamically generated html comments c094b0d
@omarkhan omarkhan ie helper allows for dynamic conditions f667ee7
@omarkhan omarkhan Fixed coffeescript helper b817c05
@omarkhan omarkhan Default doctype 147b194
@omarkhan omarkhan Tests run from command line 83ab4a1
@omarkhan omarkhan Self-closing tag tweak so that test passes 7d38b5d
@omarkhan omarkhan Optimized compiler does not escape literal strings in the template, t…
…ests adjusted accordingly
39a48f0
@omarkhan omarkhan text() function accepts arrays 196fb6f
@omarkhan omarkhan Code object can generate if blocks e0b07c4
@omarkhan omarkhan Functions passed as object attributes are prerendered as text 7d2ec29
@omarkhan omarkhan Minor tweak to expected test result e384f60
@omarkhan omarkhan Minor refactor 064c2c5
@omarkhan omarkhan Modified ie conditional comment test: formatting not yet supported 0b507bb
@omarkhan omarkhan Added optimize option 8d4f3ae
@omarkhan omarkhan Created separate test suite for optimized templates edcbd37
@omarkhan omarkhan Do not optimize templates when cache is off db169a2
@omarkhan omarkhan Benchmark optimized templates af483dc
@omarkhan omarkhan Escape hardcoded string literals cb44056
@omarkhan omarkhan Fixed tests 1dacf02
@omarkhan omarkhan Updated package.json d23557e
@omarkhan omarkhan text() does not render arrays 41653d7
@omarkhan omarkhan Use string concatenation instead of array join e7e992f
@omarkhan omarkhan Added precompiled eco template benchmark 6ca8b69
@omarkhan omarkhan Bugfix: missing 'var' before skeleton variables 2285f55
@flosse

Great work!

@wmertens

I wish github had a +1 button, this is awesome! I know it's in https://github.com/gradus/coffeecup now but I just wanted to express my awe :-)

@gradus

@wmertens

It does. Just type.

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 17, 2011
  1. @omarkhan

    Simplified executable

    omarkhan authored
  2. @omarkhan
Commits on Oct 18, 2011
  1. @omarkhan

    Get exports from main script

    omarkhan authored
  2. @omarkhan

    Removed function wrapper

    omarkhan authored
  3. @omarkhan

    Added hardcoded locals

    omarkhan authored
  4. @omarkhan
  5. @omarkhan
  6. @omarkhan
  7. @omarkhan

    Fixed function wrapper

    omarkhan authored
  8. @omarkhan
  9. @omarkhan

    Added doctype

    omarkhan authored
  10. @omarkhan
  11. @omarkhan
  12. @omarkhan
  13. @omarkhan
  14. @omarkhan

    Code cleanup, comments

    omarkhan authored
  15. @omarkhan
  16. @omarkhan

    Added tag() function

    omarkhan authored
  17. @omarkhan

    Added comment() function

    omarkhan authored
  18. @omarkhan
Commits on Oct 19, 2011
  1. @omarkhan

    Refactor

    omarkhan authored
  2. @omarkhan
  3. @omarkhan

    Comments for clarity

    omarkhan authored
  4. @omarkhan
  5. @omarkhan
  6. @omarkhan

    Added locals option

    omarkhan authored
  7. @omarkhan
  8. @omarkhan
  9. @omarkhan
  10. @omarkhan
  11. @omarkhan
  12. @omarkhan

    Added autoescape option

    omarkhan authored
Commits on Oct 20, 2011
  1. @omarkhan

    Do not escape calls to yield

    omarkhan authored
  2. @omarkhan
  3. @omarkhan
  4. @omarkhan
  5. @omarkhan
  6. @omarkhan

    Fixed coffeescript helper

    omarkhan authored
  7. @omarkhan

    Default doctype

    omarkhan authored
  8. @omarkhan

    Tests run from command line

    omarkhan authored
  9. @omarkhan
  10. @omarkhan
  11. @omarkhan
  12. @omarkhan
  13. @omarkhan
  14. @omarkhan
  15. @omarkhan

    Minor refactor

    omarkhan authored
  16. @omarkhan
  17. @omarkhan

    Added optimize option

    omarkhan authored
  18. @omarkhan
  19. @omarkhan
  20. @omarkhan

    Benchmark optimized templates

    omarkhan authored
  21. @omarkhan
  22. @omarkhan

    Fixed tests

    omarkhan authored
  23. @omarkhan

    Updated package.json

    omarkhan authored
  24. @omarkhan

    text() does not render arrays

    omarkhan authored
Commits on Oct 21, 2011
  1. @omarkhan
Commits on Oct 22, 2011
  1. @omarkhan
Commits on Oct 25, 2011
  1. @omarkhan
This page is out of date. Refresh to see the latest.
View
40 benchmark.coffee
@@ -1,3 +1,5 @@
+#!/usr/bin/env coffee
+
coffeekup = require './src/coffeekup'
jade = require 'jade'
ejs = require 'ejs'
@@ -63,6 +65,8 @@ coffeekup_string_template = """
coffeekup_compiled_template = coffeekup.compile coffeekup_template
+coffeekup_optimized_template = coffeekup.compile coffeekup_template, optimize: yes
+
jade_template = '''
!!! 5
html(lang="en")
@@ -153,6 +157,8 @@ eco_template = '''
</html>
'''
+eco_compiled_template = eco.compile eco_template
+
haml_template = '''
!!! 5
%html{lang: "en"}
@@ -185,23 +191,25 @@ benchmark = (title, code) ->
code()
log "#{title}: #{new Date - start} ms"
-@run = ->
- benchmark 'CoffeeKup (precompiled)', -> coffeekup_compiled_template data
- benchmark 'Jade (precompiled)', -> jade_compiled_template data
- benchmark 'haml-js (precompiled)', -> haml_template_compiled data
- benchmark 'Eco', -> eco.render eco_template, data
- console.log '\n'
+benchmark 'CoffeeKup (precompiled)', -> coffeekup_compiled_template data
+benchmark 'CoffeeKup (precompiled, optimized)', -> coffeekup_optimized_template data
+benchmark 'Jade (precompiled)', -> jade_compiled_template data
+benchmark 'haml-js (precompiled)', -> haml_template_compiled data
+benchmark 'Eco (precompiled)', -> eco_compiled_template data
+
+console.log '\n'
- benchmark 'CoffeeKup (function, cache on)', -> coffeekup.render coffeekup_template, data, cache: on
- benchmark 'CoffeeKup (string, cache on)', -> coffeekup.render coffeekup_string_template, data, cache: on
- benchmark 'Jade (cache on)', -> jade.render jade_template, locals: data, cache: on, filename: 'test'
- benchmark 'ejs (cache on)', -> ejs.render ejs_template, locals: data, cache: on, filename: 'test'
+benchmark 'CoffeeKup (function, cache on)', -> coffeekup.render coffeekup_template, data, cache: on
+benchmark 'CoffeeKup (string, cache on)', -> coffeekup.render coffeekup_string_template, data, cache: on
+#benchmark 'Jade (cache on)', -> jade.render jade_template, locals: data, cache: on, filename: 'test'
+benchmark 'ejs (cache on)', -> ejs.render ejs_template, locals: data, cache: on, filename: 'test'
+benchmark 'Eco', -> eco.render eco_template, data
- console.log '\n'
+console.log '\n'
- benchmark 'CoffeeKup (function, cache off)', -> coffeekup.render coffeekup_template, data
- benchmark 'CoffeeKup (string, cache off)', -> coffeekup.render coffeekup_string_template, data, cache: off
- benchmark 'Jade (cache off)', -> jade.render jade_template, locals: data
- benchmark 'haml-js', -> haml.render haml_template, locals: data
- benchmark 'ejs (cache off)', -> ejs.render ejs_template, locals: data
+benchmark 'CoffeeKup (function, cache off)', -> coffeekup.render coffeekup_template, data, cache: off
+benchmark 'CoffeeKup (string, cache off)', -> coffeekup.render coffeekup_string_template, data, cache: off
+#benchmark 'Jade (cache off)', -> jade.render jade_template, locals: data
+benchmark 'haml-js', -> haml.render haml_template, locals: data
+benchmark 'ejs (cache off)', -> ejs.render ejs_template, locals: data
View
8 bin/coffeekup
@@ -1,7 +1,3 @@
-#!/usr/bin/env node
+#!/usr/bin/env coffee
-var path = require('path')
-var fs = require('fs')
-var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib')
-
-require(lib + '/cli').run()
+require(__dirname + '/../src/cli').run()
View
2  package.json
@@ -9,7 +9,7 @@
"devDependencies": {"jade": "0.13.0", "eco": "1.1.0-rc-1", "ejs": "0.4.3", "haml": "0.4.2"},
"keywords": ["template", "view", "coffeescript"],
"bin": "./bin/coffeekup",
- "main": "./lib/coffeekup",
+ "main": "./src/coffeekup",
"engines": {"node": ">= 0.4.7"},
"contributors": [
"Luis Pedro Coelho <lpc@cmu.edu>",
View
13 src/coffeekup.coffee
@@ -15,6 +15,8 @@ if window?
else
coffeekup = exports
coffee = require 'coffee-script'
+ compiler = require __dirname + '/compiler'
+ compiler.setup coffeekup
coffeekup.version = '0.3.1edge'
@@ -294,6 +296,11 @@ coffeekup.compile = (template, options = {}) ->
hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};"
else hardcoded_locals += "var #{k} = #{JSON.stringify v};"
+ # If `optimize` is set on the options hash, use uglify-js to parse the
+ # template function's code and optimize it using static analysis.
+ if options.optimize and compiler?
+ return compiler.compile template, hardcoded_locals, options
+
# Add a function for each tag this template references. We don't want to have
# all hundred-odd tags wasting space in the compiled function.
tag_functions = ''
@@ -338,6 +345,10 @@ coffeekup.render = (template, data = {}, options = {}) ->
data[k] = v for k, v of options
data.cache ?= off
+ # Do not optimize templates if the cache is disabled, as it will slow
+ # everything down considerably.
+ if data.optimize and not data.cache then data.optimize = no
+
if data.cache and cache[template]? then tpl = cache[template]
else if data.cache then tpl = cache[template] = coffeekup.compile(template, data)
else tpl = coffeekup.compile(template, data)
@@ -368,4 +379,4 @@ unless window?
return ->
try tpl arguments...
- catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"
+ catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"
View
340 src/compiler.coffee
@@ -0,0 +1,340 @@
+coffee = require 'coffee-script'
+{uglify, parser} = require 'uglify-js'
+coffeekup = null
+
+# Call this from the main script so that the compiler module can have access to
+# coffeekup exports (node does not allow circular imports).
+exports.setup = (ck) ->
+ coffeekup = ck
+
+skeleton = '''
+ var __ck = {
+ buffer: ''
+ };
+ var text = function(txt) {
+ if (typeof txt === 'string' || txt instanceof String) {
+ __ck.buffer += txt;
+ } else if (typeof txt === 'number' || txt instanceof Number) {
+ __ck.buffer += String(txt);
+ }
+ };
+ var h = function(txt) {
+ var escaped;
+ if (typeof txt === 'string' || txt instanceof String) {
+ escaped = txt.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;');
+ } else {
+ escaped = txt;
+ }
+ return escaped;
+ };
+ var yield = function(f) {
+ var temp_buffer = '';
+ var old_buffer = __ck.buffer;
+ __ck.buffer = temp_buffer;
+ f();
+ temp_buffer = __ck.buffer;
+ __ck.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
+ else
+ @line += str
+
+ # Flush the buffered line to the array of nodes
+ flush: ->
+ if @block?
+ @block.flush()
+ else
+ @nodes.push @call ['string', @line]
+ @line = ''
+
+ # Wrap subsequent calls to `text()` in an if block
+ open_if: (condition) ->
+ @flush()
+ if @block?
+ @block.open_if condition
+ else
+ @block = new Code()
+ @block.condition = condition
+
+ # Close an if block
+ close_if: ->
+ @flush()
+ if @block.block?
+ @block.close_if()
+ else
+ @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) ->
+ @flush()
+ if @block?
+ @block.push node
+ else
+ @nodes.push @call node
+
+ # 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: ->
+ @flush()
+
+ if @parent[0] is 'stat'
+ return ['splice', @nodes]
+
+ return call_bound_func([
+ 'function'
+ null # Anonymous function
+ [] # Takes no arguments
+ @nodes
+ ])
+
+
+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 = String(args[0][1])
+ if doctype of coffeekup.doctypes
+ code.append coffeekup.doctypes[doctype]
+ else
+ throw new Error 'Invalid doctype'
+ else
+ code.append coffeekup.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]}-->"
+ else
+ 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]}]>"
+ else
+ 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 coffeekup.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
+ else
+ 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']
+ continue
+
+ # 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 '"'
+ code.close_if()
+
+ # 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 + '-'
+
+ else
+ 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 '#', ''
+ else
+ 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.
+ else
+ 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 `yield`.
+ 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 'yield'
+ return node
+ return escape node
+ else
+ 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.
+ else
+ contents = escape w.walk arg
+
+ if name in coffeekup.self_closing
+ code.append ' />'
+ else
+ code.append '>'
+
+ code.push contents if contents?
+ if not (name in coffeekup.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 __ck.buffer;"
+
+ return new Function 'data', code
+
View
6 test.coffee
@@ -1,3 +1,5 @@
+#!/usr/bin/env coffee
+
tests =
'Literal text':
template: "text 'Just text'"
@@ -218,4 +220,6 @@ render = ck.render
print "- #{name}:\n"
print t.template + "\n"
printc 'green', t.expected + "\n"
- printc 'redder', t.result.stack + "\n\n"
+ printc 'redder', t.result.stack + "\n\n"
+
+@run()
View
231 test_optimized.coffee
@@ -0,0 +1,231 @@
+#!/usr/bin/env coffee
+
+tests =
+ 'Literal text':
+ template: "text 'Just text'"
+ expected: 'Just text'
+
+ 'Default DOCTYPE':
+ template: "doctype()"
+ expected: '<!DOCTYPE html>'
+
+ 'DOCTYPE':
+ template: "doctype 'xml'"
+ expected: '<?xml version="1.0" encoding="utf-8" ?>'
+
+ 'Custom tag':
+ template: "tag 'custom'"
+ expected: '<custom></custom>'
+
+ 'Custom tag with attributes':
+ template: "tag 'custom', foo: 'bar', ping: 'pong'"
+ expected: '<custom foo="bar" ping="pong"></custom>'
+
+ 'Custom tag with attributes and inner content':
+ template: "tag 'custom', foo: 'bar', ping: 'pong', -> 'zag'"
+ expected: '<custom foo="bar" ping="pong">zag</custom>'
+
+ 'Self-closing tags':
+ template: "img src: 'icon.png', alt: 'Icon'"
+ expected: '<img src="icon.png" alt="Icon" />'
+
+ 'Common tag':
+ template: "p 'hi'"
+ expected: '<p>hi</p>'
+
+ 'Attributes':
+ template: "a href: '/', title: 'Home'"
+ expected: '<a href="/" title="Home"></a>'
+
+ 'HereDocs':
+ template: '''
+ script """
+ $(document).ready(function(){
+ alert('test');
+ });
+ """
+ '''
+ expected: "<script>$(document).ready(function(){\n alert('test');\n});</script>"
+
+ 'CoffeeScript helper (function)':
+ template: "coffeescript -> alert 'hi'"
+ expected: "<script>(function() {\n return alert(\"hi\");\n}).call(this);</script>"
+
+ 'CoffeeScript helper (string)':
+ template: "coffeescript \"alert 'hi'\""
+ expected: "<script>alert('hi');</script>"
+
+ 'Context vars':
+ template: "h1 @foo"
+ expected: '<h1>bar</h1>'
+ params: {foo: 'bar'}
+
+ 'Local vars, hardcoded':
+ template: 'h1 "harcoded: " + obj.foo'
+ run: ->
+ obj = {foo: 'bar'}
+ @compiled = ck.compile(@template, hardcode: {obj})
+ @expected = '<h1>harcoded: bar</h1>'
+ @result = @compiled()
+ @success = @result is @expected
+ if @success
+ obj.foo = 'baz'
+ @result = @compiled()
+ @success = @result is @expected
+
+ 'Local vars, hard-coded (functions)':
+ template: "h1 \"The sum is: \#{sum 1, 2}\""
+ expected: '<h1>The sum is: 3</h1>'
+ params: {hardcode: {sum: (a, b) -> a + b}}
+
+ 'Local vars, hard-coded ("helpers")':
+ template: "textbox id: 'foo'"
+ expected: '<input id="foo" name="foo" type="text" />'
+ params:
+ hardcode:
+ textbox: (attrs) ->
+ tag 'input',
+ id: attrs.id
+ name: attrs.id
+ type: 'text'
+
+ 'Local vars':
+ template: 'h1 "dynamic: " + obj.foo'
+ run: ->
+ obj = {foo: 'bar'}
+ @expected = '<h1>dynamic: bar</h1>'
+ @result = render(@template, locals: {obj: obj})
+ @success = @result is @expected
+ if @success
+ obj.foo = 'baz'
+ @expected = '<h1>dynamic: baz</h1>'
+ @result = render(@template, locals: {obj: obj})
+ @success = @result is @expected
+
+ 'Comments':
+ template: "comment 'Comment'"
+ expected: '<!--Comment-->'
+
+ 'Escaping':
+ template: "h1 h(\"<script>alert('\\\"pwned\\\" by c&a &copy;')</script>\")"
+ expected: "<h1>&lt;script&gt;alert('&quot;pwned&quot; by c&amp;a &amp;copy;')&lt;/script&gt;</h1>"
+
+ 'Autoescaping':
+ template: "h1 \"<script>alert('\\\"pwned\\\" by c&a &copy;')</script>\""
+ expected: "<h1>&lt;script&gt;alert('&quot;pwned&quot; by c&amp;a &amp;copy;')&lt;/script&gt;</h1>"
+ params:
+ autoescape: yes
+
+ 'ID/class shortcut (combo)':
+ template: "div '#myid.myclass1.myclass2', 'foo'"
+ expected: '<div id="myid" class="myclass1 myclass2">foo</div>'
+
+ 'ID/class shortcut (ID only)':
+ template: "div '#myid', 'foo'"
+ expected: '<div id="myid">foo</div>'
+
+ 'ID/class shortcut (one class only)':
+ template: "div '.myclass', 'foo'"
+ expected: '<div class="myclass">foo</div>'
+
+ 'ID/class shortcut (multiple classes)':
+ template: "div '.myclass.myclass2.myclass3', 'foo'"
+ expected: '<div class="myclass myclass2 myclass3">foo</div>'
+
+ 'ID/class shortcut (no string contents)':
+ template: "img '#myid.myclass', src: '/pic.png'"
+ expected: '<img id="myid" class="myclass" src="/pic.png" />'
+
+ 'Attribute values':
+ template: "br vrai: yes, faux: no, undef: @foo, nil: null, str: 'str', num: 42, arr: [1, 2, 3].join(','), obj: {foo: 'bar'}, func: ->"
+ expected: '<br vrai="vrai" str="str" num="42" arr="1,2,3" obj-foo="bar" func="(function(){}).call(this);" />'
+
+ 'IE conditionals':
+ template: """
+ html ->
+ head ->
+ title 'test'
+ ie 'gte IE8', ->
+ link href: 'ie.css', rel: 'stylesheet'
+ """
+ expected: '<html><head><title>test</title><!--[if gte IE8]><link href="ie.css" rel="stylesheet" /><![endif]--></head></html>'
+ #expected: '''
+ # <html>
+ # <head>
+ # <title>test</title>
+ # <!--[if gte IE8]>
+ # <link href="ie.css" rel="stylesheet" />
+ # <![endif]-->
+ # </head>
+ # </html>
+ #
+ #'''
+ #params: {format: yes}
+
+ 'yield':
+ template: "p \"This text could use \#{yield -> strong -> a href: '/', 'a link'}.\""
+ expected: '<p>This text could use <strong><a href="/">a link</a></strong>.</p>'
+
+ck = require './src/coffeekup'
+render = ck.render
+
+@run = ->
+ {print} = require 'sys'
+ colors = {red: "\033[31m", redder: "\033[91m", green: "\033[32m", normal: "\033[0m"}
+ printc = (color, str) -> print colors[color] + str + colors.normal
+
+ [total, passed, failed, errors] = [0, [], [], []]
+
+ for name, test of tests
+ total++
+ try
+ if not test.params?
+ test.params =
+ optimize: true
+ cache: on
+ else
+ test.params.optimize = true
+ test.params.cache = true
+ test.original_params = JSON.stringify test.params
+
+ if test.run
+ test.run()
+ else
+ test.result = ck.render(test.template, test.params)
+ test.success = test.result is test.expected
+
+ if test.success
+ passed.push name
+ print "[Passed] #{name}\n"
+ else
+ failed.push name
+ printc 'red', "[Failed] #{name}\n"
+ catch ex
+ test.result = ex
+ errors.push name
+ printc 'redder', "[Error] #{name}\n"
+
+ print "\n#{total} tests, #{passed.length} passed, #{failed.length} failed, #{errors.length} errors\n\n"
+
+ if failed.length > 0
+ printc 'red', "FAILED:\n\n"
+
+ for name in failed
+ t = tests[name]
+ print "- #{name}:\n"
+ print t.template + "\n"
+ print t.original_params + "\n" if t.params
+ printc 'green', t.expected + "\n"
+ printc 'red', t.result + "\n\n"
+
+ if errors.length > 0
+ printc 'redder', "ERRORS:\n\n"
+
+ for name in errors
+ t = tests[name]
+ print "- #{name}:\n"
+ print t.template + "\n"
+ printc 'green', t.expected + "\n"
+ printc 'redder', t.result.stack + "\n\n"
+
+@run()
Something went wrong with that request. Please try again.