From 607149a07db85e45e6ebad8b9dc34c5ac20e79a1 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Mon, 12 Dec 2011 16:24:55 -0800 Subject: [PATCH] Half-baked template experiment --- spec/atom/app-spec.coffee | 11 + spec/spec-suite.coffee | 4 +- spec/stdlib/template-spec.coffee | 20 ++ src/stdlib/template.coffee | 11 + vendor/coffeekup.coffee | 367 +++++++++++++++++++++++++++++++ 5 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 spec/atom/app-spec.coffee create mode 100644 spec/stdlib/template-spec.coffee create mode 100644 src/stdlib/template.coffee create mode 100644 vendor/coffeekup.coffee diff --git a/spec/atom/app-spec.coffee b/spec/atom/app-spec.coffee new file mode 100644 index 000000000..03e5bc06a --- /dev/null +++ b/spec/atom/app-spec.coffee @@ -0,0 +1,11 @@ +App = require 'views/app' + +describe "App", -> + view = null + + beforeEach -> + view = App.buildView() + + + + diff --git a/spec/spec-suite.coffee b/spec/spec-suite.coffee index b92ec40f1..4e5e11efa 100644 --- a/spec/spec-suite.coffee +++ b/spec/spec-suite.coffee @@ -1,4 +1,2 @@ -describe "a test", -> - it "should run", -> - (expect 1).toEqual 2 +require 'template-spec' diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee new file mode 100644 index 000000000..1a139ffd0 --- /dev/null +++ b/spec/stdlib/template-spec.coffee @@ -0,0 +1,20 @@ +Template = require 'template' + +describe "Template", -> + describe "toView", -> + Foo = null + + beforeEach -> + class Foo extends Template + content: -> + div -> + h1 @title + + afterEach -> + delete window.Foo + + it "builds a jquery object based on the content method and extends it with the viewProperties", -> + view = Foo.buildView(title: "Hello World") + expect(view.find('h1').text()).toEqual "Hello World" + + diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee new file mode 100644 index 000000000..8ce3a4b31 --- /dev/null +++ b/src/stdlib/template.coffee @@ -0,0 +1,11 @@ +$ = require 'jquery' +coffeekup = require 'coffeekup' + +module.exports = +class Template + @buildView: (attributes) -> + (new this).buildView(attributes) + + buildView: (attributes) -> + $(coffeekup.render(@content, attributes)) + diff --git a/vendor/coffeekup.coffee b/vendor/coffeekup.coffee new file mode 100644 index 000000000..4156a4c9d --- /dev/null +++ b/vendor/coffeekup.coffee @@ -0,0 +1,367 @@ +# **CoffeeKup** lets you to write HTML templates in 100% pure +# [CoffeeScript](http://coffeescript.org). +# +# You can run it on [node.js](http://nodejs.org) or the browser, or compile your +# templates down to self-contained javascript functions, that will take in data +# and options and return generated HTML on any JS runtime. +# +# The concept is directly stolen from the amazing +# [Markaby](http://markaby.rubyforge.org/) by Tim Fletcher and why the lucky +# stiff. + +coffeekup = module.exports +coffee = require 'coffee-script' + +coffeekup.version = '0.3.1edge' + +# Values available to the `doctype` function inside a template. +# Ex.: `doctype 'strict'` +coffeekup.doctypes = + 'default': '' + '5': '' + 'xml': '' + 'transitional': '' + 'strict': '' + 'frameset': '' + '1.1': '', + 'basic': '' + 'mobile': '' + 'ce': '' + +# CoffeeScript-generated JavaScript may contain anyone of these; but when we +# take a function to string form to manipulate it, and then recreate it through +# the `Function()` constructor, it loses access to its parent scope and +# consequently to any helpers it might need. So we need to reintroduce these +# inside any "rewritten" function. +coffeescript_helpers = """ + var __slice = Array.prototype.slice; + var __hasProp = Object.prototype.hasOwnProperty; + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var __extends = function(child, parent) { + for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; + return child; }; + var __indexOf = Array.prototype.indexOf || function(item) { + for (var i = 0, l = this.length; i < l; i++) { + if (this[i] === item) return i; + } return -1; }; +""".replace /\n/g, '' + +# Private HTML element reference. +# Please mind the gap (1 space at the beginning of each subsequent line). +elements = + # Valid HTML 5 elements requiring a closing tag. + # Note: the `var` element is out for obvious reasons, please use `tag 'var'`. + regular: 'a abbr address article aside audio b bdi bdo blockquote body button + canvas caption cite code colgroup datalist dd del details dfn div dl dt em + fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup + html i iframe ins kbd label legend li map mark menu meter nav noscript object + ol optgroup option output p pre progress q rp rt ruby s samp script section + select small span strong style sub summary sup table tbody td textarea tfoot + th thead time title tr u ul video' + + # Valid self-closing HTML 5 elements. + void: 'area base br col command embed hr img input keygen link meta param + source track wbr' + + obsolete: 'applet acronym bgsound dir frameset noframes isindex listing + nextid noembed plaintext rb strike xmp big blink center font marquee multicol + nobr spacer tt' + + obsolete_void: 'basefont frame' + +# Create a unique list of element names merging the desired groups. +merge_elements = (args...) -> + result = [] + for a in args + for element in elements[a].split ' ' + result.push element unless element in result + result + +# Public/customizable list of possible elements. +# For each name in this list that is also present in the input template code, +# a function with the same name will be added to the compiled template. +coffeekup.tags = merge_elements 'regular', 'obsolete', 'void', 'obsolete_void' + +# Public/customizable list of elements that should be rendered self-closed. +coffeekup.self_closing = merge_elements 'void', 'obsolete_void' + +# This is the basic material from which compiled templates will be formed. +# It will be manipulated in its string form at the `coffeekup.compile` function +# to generate the final template function. +skeleton = (data = {}) -> + # Whether to generate formatted HTML with indentation and line breaks, or + # just the natural "faux-minified" output. + data.format ?= off + + # Whether to autoescape all content or let you handle it on a case by case + # basis with the `h` function. + data.autoescape ?= off + + # Internal CoffeeKup stuff. + __ck = + buffer: [] + + esc: (txt) -> + if data.autoescape then h(txt) else String(txt) + + tabs: 0 + + repeat: (string, count) -> Array(count + 1).join string + + indent: -> text @repeat(' ', @tabs) if data.format + + # Adapter to keep the builtin tag functions DRY. + tag: (name, args) -> + combo = [name] + combo.push i for i in args + tag.apply data, combo + + render_idclass: (str) -> + classes = [] + + for i in str.split '.' + if '#' in i + id = i.replace '#', '' + else + classes.push i unless i is '' + + text " id=\"#{id}\"" if id + + if classes.length > 0 + text " class=\"" + for c in classes + text ' ' unless c is classes[0] + text c + text '"' + + render_attrs: (obj, prefix = '') -> + for k, v of obj + # `true` is rendered as `selected="selected"`. + v = k if typeof v is 'boolean' and v + + # Functions are rendered in an executable form. + v = "(#{v}).call(this);" if typeof v is 'function' + + # Prefixed attribute. + if typeof v is 'object' and v not instanceof Array + # `data: {icon: 'foo'}` is rendered as `data-icon="foo"`. + @render_attrs(v, prefix + k + '-') + # `undefined`, `false` and `null` result in the attribute not being rendered. + else if v + # strings, numbers, arrays and functions are rendered "as is". + text " #{prefix + k}=\"#{@esc(v)}\"" + + render_contents: (contents) -> + switch typeof contents + when 'string', 'number', 'boolean' + text @esc(contents) + when 'function' + text '\n' if data.format + @tabs++ + result = contents.call data + if typeof result is 'string' + @indent() + text @esc(result) + text '\n' if data.format + @tabs-- + @indent() + + render_tag: (name, idclass, attrs, contents) -> + @indent() + + text "<#{name}" + @render_idclass(idclass) if idclass + @render_attrs(attrs) if attrs + + if name in @self_closing + text ' />' + text '\n' if data.format + else + text '>' + + @render_contents(contents) + + text "" + text '\n' if data.format + + null + + tag = (name, args...) -> + for a in args + switch typeof a + when 'function' + contents = a + when 'object' + attrs = a + when 'number', 'boolean' + contents = a + when 'string' + if args.length is 1 + contents = a + else + if a is args[0] + idclass = a + else + contents = a + + __ck.render_tag(name, idclass, attrs, contents) + + yield = (f) -> + temp_buffer = [] + old_buffer = __ck.buffer + __ck.buffer = temp_buffer + f() + __ck.buffer = old_buffer + temp_buffer.join '' + + h = (txt) -> + String(txt).replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + + doctype = (type = 'default') -> + text __ck.doctypes[type] + text '\n' if data.format + + text = (txt) -> + __ck.buffer.push String(txt) + null + + comment = (cmt) -> + text "" + text '\n' if data.format + + coffeescript = (param) -> + switch typeof param + # `coffeescript -> alert 'hi'` becomes: + # `` + when 'function' + script "#{__ck.coffeescript_helpers}(#{param}).call(this);" + # `coffeescript "alert 'hi'"` becomes: + # `` + when 'string' + script type: 'text/coffeescript', -> param + # `coffeescript src: 'script.coffee'` becomes: + # `` + when 'object' + param.type = 'text/coffeescript' + script param + + # Conditional IE comments. + ie = (condition, contents) -> + __ck.indent() + + text "" + text '\n' if data.format + + null + +# Stringify the skeleton and unwrap it from its enclosing `function(){}`, then +# add the CoffeeScript helpers. +skeleton = String(skeleton) + .replace(/function\s*\(.*\)\s*\{/, '') + .replace(/return null;\s*\}$/, '') + +skeleton = coffeescript_helpers + skeleton + +# Compiles a template into a standalone JavaScript function. +coffeekup.compile = (template, options = {}) -> + # The template can be provided as either a function or a CoffeeScript string + # (in the latter case, the CoffeeScript compiler must be available). + if typeof template is 'function' then template = String(template) + else if typeof template is 'string' and coffee? + template = coffee.compile template, bare: yes + template = "function(){#{template}}" + + # If an object `hardcode` is provided, insert the stringified value + # of each variable directly in the function body. This is a less flexible but + # faster alternative to the standard method of using `with` (see below). + hardcoded_locals = '' + + if options.hardcode + for k, v of options.hardcode + if typeof v is 'function' + # Make sure these functions have access to `data` as `@/this`. + hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};" + else hardcoded_locals += "var #{k} = #{JSON.stringify v};" + + # 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 = '' + tags_used = [] + + for t in coffeekup.tags + if template.indexOf(t) > -1 or hardcoded_locals.indexOf(t) > -1 + tags_used.push t + + tag_functions += "var #{tags_used.join ','};" + for t in tags_used + tag_functions += "#{t} = function(){return __ck.tag('#{t}', arguments);};" + + # Main function assembly. + code = tag_functions + hardcoded_locals + skeleton + + code += "__ck.doctypes = #{JSON.stringify coffeekup.doctypes};" + code += "__ck.coffeescript_helpers = #{JSON.stringify coffeescript_helpers};" + code += "__ck.self_closing = #{JSON.stringify coffeekup.self_closing};" + + # If `locals` is set, wrap the template inside a `with` block. This is the + # most flexible but slower approach to specifying local variables. + code += 'with(data.locals){' if options.locals + code += "(#{template}).call(data);" + code += '}' if options.locals + code += "return __ck.buffer.join('');" + + new Function('data', code) + +cache = {} + +# Template in, HTML out. Accepts functions or strings as does `coffeekup.compile`. +# +# Accepts an option `cache`, by default `false`. If set to `false` templates will +# be recompiled each time. +# +# `options` is just a convenience parameter to pass options separately from the +# data, but the two will be merged and passed down to the compiler (which uses +# `locals` and `hardcode`), and the template (which understands `locals`, `format` +# and `autoescape`). +coffeekup.render = (template, data = {}, options = {}) -> + data[k] = v for k, v of options + data.cache ?= off + + 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) + tpl(data) + +unless window? + coffeekup.adapters = + # Legacy adapters for when CoffeeKup expected data in the `context` attribute. + simple: coffeekup.render + meryl: coffeekup.render + + express: + TemplateError: class extends Error + constructor: (@message) -> + Error.call this, @message + Error.captureStackTrace this, arguments.callee + name: 'TemplateError' + + compile: (template, data) -> + # Allows `partial 'foo'` instead of `text @partial 'foo'`. + data.hardcode ?= {} + data.hardcode.partial = -> + text @partial.apply @, arguments + + TemplateError = @TemplateError + try tpl = coffeekup.compile(template, data) + catch e then throw new TemplateError "Error compiling #{data.filename}: #{e.message}" + + return -> + try tpl arguments... + catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"