├── lib └── .gitignore ├── .npmignore ├── examples ├── express │ ├── views │ │ ├── partial.coffee │ │ ├── index.coffee │ │ ├── login.coffee │ │ └── layout.coffee │ └── app.coffee ├── browser │ ├── creme │ │ ├── coffeekup.js │ │ └── index.html │ ├── decaf │ │ ├── build │ │ ├── template.coffee │ │ ├── index.html │ │ └── template.js │ └── regular │ │ ├── template.coffee │ │ ├── build │ │ ├── index.coffee │ │ ├── index.js │ │ ├── index.html │ │ └── template.js ├── meryl │ ├── templates │ │ ├── index.coffee │ │ └── layout.coffee │ └── app.coffee └── zappa.coffee ├── .gitignore ├── bin └── coffeekup ├── Cakefile ├── LICENSE ├── package.json ├── src ├── cli.coffee └── coffeekup.coffee ├── CHANGELOG.md ├── benchmark.coffee ├── docs ├── docco.css ├── reference.md └── coffeekup.html ├── test.coffee └── README.md /lib/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | material/ 3 | -------------------------------------------------------------------------------- /examples/express/views/partial.coffee: -------------------------------------------------------------------------------- 1 | p 'Express partial' -------------------------------------------------------------------------------- /examples/browser/creme/coffeekup.js: -------------------------------------------------------------------------------- 1 | ../../../lib/coffeekup.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | material/ 3 | node_modules/ 4 | lib/*.js -------------------------------------------------------------------------------- /examples/browser/decaf/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | coffeekup --js template.coffee 3 | 4 | -------------------------------------------------------------------------------- /examples/browser/decaf/template.coffee: -------------------------------------------------------------------------------- 1 | ul -> 2 | for guy in @stooges 3 | li guy 4 | -------------------------------------------------------------------------------- /examples/browser/regular/template.coffee: -------------------------------------------------------------------------------- 1 | ul -> 2 | for guy in @stooges 3 | li guy 4 | -------------------------------------------------------------------------------- /examples/meryl/templates/index.coffee: -------------------------------------------------------------------------------- 1 | h1 'Meryl example' 2 | ul -> 3 | for name in @people 4 | li name 5 | -------------------------------------------------------------------------------- /examples/browser/regular/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | coffeekup --js template.coffee 3 | coffee -c index.coffee 4 | 5 | -------------------------------------------------------------------------------- /examples/browser/regular/index.coffee: -------------------------------------------------------------------------------- 1 | $().ready -> 2 | $('body').append templates.template(stooges: ['moe', 'larry', 'curly']) 3 | -------------------------------------------------------------------------------- /examples/meryl/templates/layout.coffee: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html -> 3 | head -> 4 | title 'Meryl example' 5 | body -> 6 | @render @content, @context 7 | -------------------------------------------------------------------------------- /bin/coffeekup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var fs = require('fs') 5 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib') 6 | 7 | require(lib + '/cli').run() 8 | -------------------------------------------------------------------------------- /examples/browser/regular/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | $().ready(function() { 3 | return $('body').append(templates.template({ 4 | stooges: ['moe', 'larry', 'curly'] 5 | })); 6 | }); 7 | }).call(this); 8 | -------------------------------------------------------------------------------- /examples/express/views/index.coffee: -------------------------------------------------------------------------------- 1 | @title = 'Chunky Bacon!' 2 | @canonical = 'http://chunky.bacon' 3 | 4 | h1 @title 5 | 6 | p 'This is the home page.' 7 | 8 | p "Let's count to 10: " 9 | 10 | p "#{i}..." for i in [1..10] 11 | 12 | partial 'partial', [1..10] -------------------------------------------------------------------------------- /examples/zappa.coffee: -------------------------------------------------------------------------------- 1 | require('zappa') -> 2 | enable 'default layout' 3 | 4 | get '/': -> 5 | @franks = ['miller', 'oz', 'sinatra', 'zappa'] 6 | render 'index' 7 | 8 | view index: -> 9 | @title = 'Zappa example' 10 | h1 @title 11 | ul -> 12 | for f in @franks 13 | li f 14 | -------------------------------------------------------------------------------- /examples/meryl/app.coffee: -------------------------------------------------------------------------------- 1 | meryl = require 'meryl' 2 | coffeekup = require '../../src/coffeekup' 3 | 4 | meryl.h 'GET /', (req, resp) -> 5 | people = ['bob', 'alice', 'meryl'] 6 | resp.render 'layout', content: 'index', context: {people: people} 7 | 8 | meryl.run 9 | templateDir: 'templates' 10 | templateExt: '.coffee' 11 | templateFunc: coffeekup.adapters.meryl 12 | 13 | console.log 'Listening on 3000...' 14 | -------------------------------------------------------------------------------- /examples/express/views/login.coffee: -------------------------------------------------------------------------------- 1 | @title = 'Log In' 2 | 3 | h1 @title 4 | 5 | p "A local var: #{ping}" 6 | p "A context var: #{@foo}" 7 | 8 | form action: '/', method: 'post', -> 9 | div class: 'field', -> 10 | label for: 'username', -> 'Username: ' 11 | input id: 'username', name: 'username' 12 | 13 | div class: 'field', -> 14 | label for: 'password', -> 'Password: ' 15 | input id: 'password', name: 'password' 16 | -------------------------------------------------------------------------------- /examples/express/app.coffee: -------------------------------------------------------------------------------- 1 | app = require('express').createServer() 2 | 3 | coffeekup = require '../../src/coffeekup' 4 | 5 | app.set 'view engine', 'coffee' 6 | app.register '.coffee', coffeekup.adapters.express 7 | 8 | app.get '/', (req, res) -> 9 | res.render 'index' 10 | 11 | app.get '/login', (req, res) -> 12 | res.render 'login', foo: 'bar', locals: {ping: 'pong'} 13 | 14 | app.get '/inline', (req, res) -> 15 | res.send coffeekup.render -> 16 | h1 'This is an inline template.' 17 | 18 | app.listen 3000 19 | 20 | console.log "Listening on 3000..." 21 | -------------------------------------------------------------------------------- /examples/browser/regular/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Writing your app and templates in CoffeeScript but pre-compiling everything on the server-side.
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/browser/decaf/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Consuming a template from a non-CoffeeScript app.
21 | 22 | 23 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn, exec} = require 'child_process' 2 | log = console.log 3 | 4 | task 'build', -> 5 | run 'coffee -o lib -c src/*.coffee' 6 | 7 | task 'test', -> require('./test').run() 8 | 9 | task 'bench', -> require('./benchmark').run() 10 | 11 | task 'docs', -> 12 | run 'docco src/coffeekup.coffee' 13 | 14 | run = (args...) -> 15 | for a in args 16 | switch typeof a 17 | when 'string' then command = a 18 | when 'object' 19 | if a instanceof Array then params = a 20 | else options = a 21 | when 'function' then callback = a 22 | 23 | command += ' ' + params.join ' ' if params? 24 | cmd = spawn '/bin/sh', ['-c', command], options 25 | cmd.stdout.on 'data', (data) -> process.stdout.write data 26 | cmd.stderr.on 'data', (data) -> process.stderr.write data 27 | process.on 'SIGHUP', -> cmd.kill() 28 | cmd.on 'exit', (code) -> callback() if callback? and code is 0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Maurice MachadoDoing everything on the client. Lots of flexibility but a fatty download.
39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/express/views/layout.coffee: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html -> 3 | head -> 4 | meta charset: 'utf-8' 5 | 6 | title "#{@title} | My Site" if @title? 7 | meta(name: 'description', content: @description) if @description? 8 | link(rel: 'canonical', href: @canonical) if @canonical? 9 | 10 | link rel: 'icon', href: '/favicon.png' 11 | link rel: 'stylesheet', href: '/app.css' 12 | 13 | script src: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js' 14 | script src: '/app.js' 15 | 16 | coffeescript -> 17 | $(document).ready -> 18 | alert 'hi!' 19 | 20 | style ''' 21 | header, nav, section, article, aside, footer {display: block} 22 | nav li {display: inline} 23 | nav.sub {float: right} 24 | #content {margin-left: 120px} 25 | ''' 26 | body -> 27 | header -> 28 | a href: '/', title: 'Home', -> 'Home' 29 | 30 | nav -> 31 | ul -> 32 | for item in ['About', 'Pricing', 'Contact'] 33 | li -> a href: "/#{item.toLowerCase()}", title: item, -> item 34 | 35 | li -> a href: '/about', title: 'About', -> 'About' 36 | li -> a href: '/pricing', title: 'Pricing', -> 'Pricing' 37 | li -> a href: '/contact', title: 'Contact Us', -> 'Contact Us' 38 | 39 | div id: 'content', -> 40 | @body 41 | 42 | footer -> 43 | p -> a href: '/privacy', -> 'Privacy Policy' 44 | -------------------------------------------------------------------------------- /src/cli.coffee: -------------------------------------------------------------------------------- 1 | coffeekup = require './coffeekup' 2 | fs = require 'fs' 3 | path = require 'path' 4 | puts = console.log 5 | {OptionParser} = require 'coffee-script/lib/optparse' 6 | 7 | argv = process.argv[2..] 8 | options = null 9 | 10 | handle_error = (err) -> console.log err.stack if err 11 | 12 | compile = (input_path, output_directory, js, namespace = 'templates') -> 13 | fs.readFile input_path, 'utf-8', (err, contents) -> 14 | handle_error err 15 | 16 | name = path.basename input_path, path.extname(input_path) 17 | 18 | if not js 19 | output = coffeekup.render contents, options 20 | ext = '.html' 21 | else 22 | func = coffeekup.compile contents, options 23 | output = """ 24 | (function(){ 25 | this.#{namespace} || (this.#{namespace} = {}); 26 | this.#{namespace}[#{JSON.stringify name}] = #{func}; 27 | }).call(this); 28 | """ 29 | ext = '.js' 30 | 31 | write input_path, name, output, output_directory, ext 32 | 33 | write = (input_path, name, contents, output_directory, ext) -> 34 | filename = name + ext 35 | dir = output_directory or path.dirname input_path 36 | path.exists dir, (exists) -> 37 | unless exists then fs.mkdirSync dir, 0777 38 | 39 | output_path = path.join dir, filename 40 | contents = ' ' if contents.length <= 0 41 | fs.writeFile output_path, contents, (err) -> 42 | handle_error err 43 | puts contents if options.print 44 | puts "Compiled #{input_path}" if options.watch 45 | 46 | usage = ''' 47 | Usage: 48 | coffeekup [options] path/to/template.coffee 49 | ''' 50 | 51 | switches = [ 52 | ['--js', 'compile template to js function'] 53 | ['-n', '--namespace [name]', 'global object holding the templates (default: "templates")'] 54 | ['-w', '--watch', 'watch templates for changes, and recompile'] 55 | ['-o', '--output [dir]', 'set the directory for compiled html'] 56 | ['-p', '--print', 'print the compiled html to stdout'] 57 | ['-f', '--format', 'apply line breaks and indentation to html output'] 58 | ['-u', '--utils', 'add helper locals (currently only "render")'] 59 | ['-v', '--version', 'display CoffeeKup version'] 60 | ['-h', '--help', 'display this help message'] 61 | ] 62 | 63 | @run = -> 64 | parser = new OptionParser switches, usage 65 | options = parser.parse argv 66 | args = options.arguments 67 | delete options.arguments 68 | 69 | puts parser.help() if options.help or argv.length is 0 70 | puts coffeekup.version if options.version 71 | if options.utils 72 | options.locals ?= {} 73 | options.locals.render = (file) -> 74 | contents = fs.readFileSync file, 'utf-8' 75 | coffeekup.render contents, options 76 | 77 | if args.length > 0 78 | file = args[0] 79 | 80 | if options.watch 81 | fs.watchFile file, {persistent: true, interval: 500}, (curr, prev) -> 82 | return if curr.size is prev.size and curr.mtime.getTime() is prev.mtime.getTime() 83 | compile file, options.output, options.js, options.namespace 84 | 85 | compile file, options.output, options.js, options.namespace 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **v0.3.1edge** (unreleased): 2 | 3 | **v0.3.1** (2011-09-29): 4 | 5 | - Fixed #71: indexOf not supported in IE7 (thanks @jaekwon). 6 | - Added better error reporting to express adapter. [benekastah] 7 | - Added `yield`: see `/docs/reference.md`. Closes #16 (thanks @pyrotechnick, @colinta and @smathy). 8 | - Added prefixed attributes: `data: {icon: 'foo.png'}` renders as `data-icon="foo.png"`. [colinta] 9 | - Added functions as attribute values: `onclick: -> alert 'hi'`. Closes #79 (thanks @smathy). 10 | 11 | **v0.3.0** (2011-09-04): 12 | 13 | - Fixed #64 (`hardcode` option masked in express adapter). [smathy] 14 | - Added missing elements from the HTML 5 spec (both valid and obsolete). Closes #66 (thanks @aeosynth). 15 | - Added compile to js option to CLI. Closes #58. [rachel-carvalho] 16 | - Fixed #69 (`coffeekup -w` only compiles upon first change). [rachel-carvalho] 17 | 18 | **v0.3.0beta** (2011-07-27): 19 | 20 | - Added documentation: API reference at `/docs/reference.md` and annotated source at `/docs/coffeekup.html`. 21 | 22 | - Added id/class shortcuts: `div '#id.class.class2', 'contents'` (thanks @aeosynth and @payload). 23 | 24 | - Added IE conditional comments: `ie 'lt IE8', -> 'IE 7 or less stuff'` (thanks @aeosynth). 25 | 26 | - Added `ck.adapters.express` which allows `partial 'foo'` instead of `text @partial 'foo'` - see `/examples/express` (thanks @cushman). 27 | 28 | - Added `coffeescript src: 'file.coffee'` and `coffeescript 'string'` - see reference (thanks @henrikh). 29 | 30 | - Changed the template param format to align with Express and other templating engines. Now `tpl(foo: 'bar')` makes `foo` accessible as `h1 @foo`. `context` is not used anymore. 31 | 32 | - `tpl(locals: {foo: 'bar'})` now always implemented through the `with` keyword (past behavior with `dynamic_locals: true`). 33 | 34 | - `tpl(hardcode: {foo: 'bar'})` will hardcode `foo` into the compiled template's body (past behavior with `dynamic_locals: false`). 35 | 36 | - Fixed: `coffeescript -> code()` now correctly adds CoffeeScript helpers to the output. 37 | 38 | - Changed: using `.call(this);` instead of `();` in js generated by `coffeescript`. 39 | 40 | - Fixed: correctly handle numbers and booleans when used as tag contents or attribute values. 41 | 42 | - Fixed #50: "`coffeekup -w` quits on syntax error". 43 | 44 | - Added: doctypes now editable at `coffeekup.doctypes`, tags at `coffeekup.tags`, and self-closing tags at `coffeekup.self_closing`. 45 | 46 | - Added the `ce` doctype. 47 | 48 | - Changed: using `doctypes['default']` instead of `doctypes['5']` by default. 49 | 50 | - Changed: in `coffeekup.render`, option `cache` is now `false` by default. 51 | 52 | - Added a third optional param to `render`. You can pass an object with options and they will be merged with the main object param. 53 | 54 | - Removed ck_* locals, now all implementation inside the `__ck` object. 55 | 56 | **v0.2.3** (2011-05-06): 57 | 58 | - Compatible with npm 1.x. 59 | - Converting any ampersands (instead of /&(?!\w+;/) to & when escaping html. 60 | - New CLI option -o / --output [DIR] (specifies a directory to compile into). 61 | - Self-closing tags are now: 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'link', 'meta' and 'param'. 62 | 63 | **v0.2.2** (2011-01-05): 64 | 65 | - Updated to CoffeeScript 1.0.0 and node 0.2.6/0.3.3. 66 | 67 | **v0.2.1** (2010-11-23): 68 | 69 | - Updated to CoffeeScript 0.9.5 and node 0.2.5/0.3.1. 70 | - Fixed string templates compilation in opera. 71 | 72 | **v0.2.0** (2010-11-09): 73 | 74 | - Huge performance gains, now among the fastest. See `cake benchmark`. 75 | - Compile templates into standalone functions with `coffeekup.compile`. 76 | - Option `format` to add line breaks and indentation to output. 77 | - Escape HTML automatically with the `autoescape` option, or manually with the `h` local. 78 | - CLI behaviour closer to CoffeeScript's: compiles to `filename.html` by default, can watch and recompile with `-w`. 79 | - CLI `-u`/`--utils` option to make build-time utility locals available to templates (currently only `render`). 80 | 81 | -------------------------------------------------------------------------------- /benchmark.coffee: -------------------------------------------------------------------------------- 1 | coffeekup = require './src/coffeekup' 2 | jade = require 'jade' 3 | ejs = require 'ejs' 4 | eco = require 'eco' 5 | haml = require 'haml' 6 | log = console.log 7 | 8 | data = 9 | title: 'test' 10 | inspired: no 11 | users: [ 12 | {email: 'house@gmail.com', name: 'house'} 13 | {email: 'cuddy@gmail.com', name: 'cuddy'} 14 | {email: 'wilson@gmail.com', name: 'wilson'} 15 | ] 16 | 17 | coffeekup_template = -> 18 | doctype 5 19 | html lang: 'en', -> 20 | head -> 21 | meta charset: 'utf-8' 22 | title @title 23 | style ''' 24 | body {font-family: "sans-serif"} 25 | section, header {display: block} 26 | ''' 27 | body -> 28 | section -> 29 | header -> 30 | h1 @title 31 | if @inspired 32 | p 'Create a witty example' 33 | else 34 | p 'Go meta' 35 | ul -> 36 | for user in @users 37 | li user.name 38 | li -> a href: "mailto:#{user.email}", -> user.email 39 | 40 | coffeekup_string_template = """ 41 | doctype 5 42 | html lang: 'en', -> 43 | head -> 44 | meta charset: 'utf-8' 45 | title @title 46 | style ''' 47 | body {font-family: "sans-serif"} 48 | section, header {display: block} 49 | ''' 50 | body -> 51 | section -> 52 | header -> 53 | h1 @title 54 | if @inspired 55 | p 'Create a witty example' 56 | else 57 | p 'Go meta' 58 | ul -> 59 | for user in @users 60 | li user.name 61 | li -> a href: "mailto:\#{user.email}", -> user.email 62 | """ 63 | 64 | coffeekup_compiled_template = coffeekup.compile coffeekup_template 65 | 66 | jade_template = ''' 67 | !!! 5 68 | html(lang="en") 69 | head 70 | meta(charset="utf-8") 71 | title= title 72 | style 73 | | body {font-family: "sans-serif"} 74 | | section, header {display: block} 75 | body 76 | section 77 | header 78 | h1= title 79 | - if (inspired) 80 | p Create a witty example 81 | - else 82 | p Go meta 83 | ul 84 | - each user in users 85 | li= user.name 86 | li 87 | a(href="mailto:"+user.email)= user.email 88 | ''' 89 | 90 | jade_compiled_template = jade.compile jade_template 91 | 92 | ejs_template = ''' 93 | 94 | 95 | 96 | 97 |Create a witty example
110 | <% } else { %> 111 |Go meta
112 | <% } %> 113 |Create a witty example
142 | <% else: %> 143 |Go meta
144 | <% end %> 145 |hi
' 33 | 34 | 'Attributes': 35 | template: "a href: '/', title: 'Home'" 36 | expected: '' 37 | 38 | 'HereDocs': 39 | template: ''' 40 | script """ 41 | $(document).ready(function(){ 42 | alert('test'); 43 | }); 44 | """ 45 | ''' 46 | expected: "" 47 | 48 | 'CoffeeScript helper (function)': 49 | template: "coffeescript -> alert 'hi'" 50 | expected: "" 51 | 52 | 'CoffeeScript helper (string)': 53 | template: "coffeescript \"alert 'hi'\"" 54 | expected: "" 55 | 56 | 'CoffeeScript helper (object)': 57 | template: "coffeescript src: 'script.coffee'" 58 | expected: "" 59 | 60 | 'Context vars': 61 | template: "h1 @foo" 62 | expected: '
'
143 |
144 | 'Attribute values':
145 | template: "br vrai: yes, faux: no, undef: @foo, nil: null, str: 'str', num: 42, arr: [1, 2, 3], obj: {foo: 'bar'}, func: ->"
146 | expected: 'This text could use a link.
' 172 | 173 | ck = require './src/coffeekup' 174 | render = ck.render 175 | 176 | @run = -> 177 | {print} = require 'sys' 178 | colors = {red: "\033[31m", redder: "\033[91m", green: "\033[32m", normal: "\033[0m"} 179 | printc = (color, str) -> print colors[color] + str + colors.normal 180 | 181 | [total, passed, failed, errors] = [0, [], [], []] 182 | 183 | for name, test of tests 184 | total++ 185 | try 186 | test.original_params = JSON.stringify test.params 187 | 188 | if test.run 189 | test.run() 190 | else 191 | test.result = ck.render(test.template, test.params) 192 | test.success = test.result is test.expected 193 | 194 | if test.success 195 | passed.push name 196 | print "[Passed] #{name}\n" 197 | else 198 | failed.push name 199 | printc 'red', "[Failed] #{name}\n" 200 | catch ex 201 | test.result = ex 202 | errors.push name 203 | printc 'redder', "[Error] #{name}\n" 204 | 205 | print "\n#{total} tests, #{passed.length} passed, #{failed.length} failed, #{errors.length} errors\n\n" 206 | 207 | if failed.length > 0 208 | printc 'red', "FAILED:\n\n" 209 | 210 | for name in failed 211 | t = tests[name] 212 | print "- #{name}:\n" 213 | print t.template + "\n" 214 | print t.original_params + "\n" if t.params 215 | printc 'green', t.expected + "\n" 216 | printc 'red', t.result + "\n\n" 217 | 218 | if errors.length > 0 219 | printc 'redder', "ERRORS:\n\n" 220 | 221 | for name in errors 222 | t = tests[name] 223 | print "- #{name}:\n" 224 | print t.template + "\n" 225 | printc 'green', t.expected + "\n" 226 | printc 'redder', t.result.stack + "\n\n" -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # CoffeeKup 0.3.1 Reference 2 | 3 | ## The CoffeeKup object 4 | 5 | Both the returned value from `require 'coffeekup'` and the global `CoffeeKup` created by ` 103 | 104 | Contents (function): 105 | 106 | div -> 'Foo' 107 |This text could use a link.
200 | 201 | Without it, the `a` function runs first, writes to the buffer and returns `null`, resulting in a useless output: 202 | 203 | p "This text could use #{a href: '/', 'a link'}." 204 | a linkThis text could use null.
205 | 206 | #### @ 207 | 208 | CoffeeScript shortcut to `this`. This is where all the input data can be accessed. 209 | 210 | ## Extending CoffeeKup 211 | 212 | template = -> 213 | h1 @title 214 | form method: 'post', action: 'login', -> 215 | textbox id: 'username' 216 | textbox id: 'password' 217 | button @title 218 | 219 | helpers = 220 | textbox: (attrs) -> 221 | attrs.type = 'text' 222 | attrs.name = attrs.id 223 | input attrs 224 | 225 | console.log CoffeeKup.render template, title: 'Log In', hardcode: helpers 226 | 227 | ## The coffeekup command 228 | 229 | When installing CoffeeKup with `npm install coffeekup -g`, you get a `coffeekup` command that allows you to generate HTML from CoffeeKup templates: 230 | 231 | $ coffeekup -h 232 | 233 | Usage: 234 | coffeekup [options] path/to/template.coffee 235 | 236 | --js compile template to js function 237 | -n, --namespace global object holding the templates (default: "templates") 238 | -w, --watch watch templates for changes, and recompile 239 | -o, --output set the directory for compiled html 240 | -p, --print print the compiled html to stdout 241 | -f, --format apply line breaks and indentation to html output 242 | -u, --utils add helper locals (currently only "render") 243 | -v, --version display CoffeeKup version 244 | -h, --help display this help message -------------------------------------------------------------------------------- /examples/browser/decaf/template.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | this.templates || (this.templates = {}); 3 | this.templates["template"] = function anonymous(data) { 4 | var a,i,li,p,s,th,u,ul;a = function(){return __ck.tag('a', arguments);};i = function(){return __ck.tag('i', arguments);};li = function(){return __ck.tag('li', arguments);};p = function(){return __ck.tag('p', arguments);};s = function(){return __ck.tag('s', arguments);};th = function(){return __ck.tag('th', arguments);};u = function(){return __ck.tag('u', arguments);};ul = function(){return __ck.tag('ul', arguments);};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; }; 5 | var coffeescript, comment, doctype, h, ie, tag, text, __ck, _ref, _ref2; 6 | if (data == null) { 7 | data = {}; 8 | } 9 | if ((_ref = data.format) != null) { 10 | _ref; 11 | } else { 12 | data.format = false; 13 | }; 14 | if ((_ref2 = data.autoescape) != null) { 15 | _ref2; 16 | } else { 17 | data.autoescape = false; 18 | }; 19 | __ck = { 20 | buffer: [], 21 | esc: function(txt) { 22 | if (data.autoescape) { 23 | return h(txt); 24 | } else { 25 | return String(txt); 26 | } 27 | }, 28 | tabs: 0, 29 | repeat: function(string, count) { 30 | return Array(count + 1).join(string); 31 | }, 32 | indent: function() { 33 | if (data.format) { 34 | return text(this.repeat(' ', this.tabs)); 35 | } 36 | }, 37 | tag: function(name, args) { 38 | var combo, i, _i, _len; 39 | combo = [name]; 40 | for (_i = 0, _len = args.length; _i < _len; _i++) { 41 | i = args[_i]; 42 | combo.push(i); 43 | } 44 | return tag.apply(data, combo); 45 | }, 46 | render_idclass: function(str) { 47 | var c, classes, i, id, _i, _j, _len, _len2, _ref3; 48 | classes = []; 49 | _ref3 = str.split('.'); 50 | for (_i = 0, _len = _ref3.length; _i < _len; _i++) { 51 | i = _ref3[_i]; 52 | if (i.indexOf('#') === 0) { 53 | id = i.replace('#', ''); 54 | } else { 55 | if (i !== '') { 56 | classes.push(i); 57 | } 58 | } 59 | } 60 | if (id) { 61 | text(" id=\"" + id + "\""); 62 | } 63 | if (classes.length > 0) { 64 | text(" class=\""); 65 | for (_j = 0, _len2 = classes.length; _j < _len2; _j++) { 66 | c = classes[_j]; 67 | if (c !== classes[0]) { 68 | text(' '); 69 | } 70 | text(c); 71 | } 72 | return text('"'); 73 | } 74 | }, 75 | render_attrs: function(obj) { 76 | var k, v, _results; 77 | _results = []; 78 | for (k in obj) { 79 | v = obj[k]; 80 | if (typeof v === 'boolean' && v) { 81 | v = k; 82 | } 83 | _results.push(v ? text(" " + k + "=\"" + (this.esc(v)) + "\"") : void 0); 84 | } 85 | return _results; 86 | }, 87 | render_contents: function(contents) { 88 | var result; 89 | switch (typeof contents) { 90 | case 'string': 91 | case 'number': 92 | case 'boolean': 93 | return text(this.esc(contents)); 94 | case 'function': 95 | if (data.format) { 96 | text('\n'); 97 | } 98 | this.tabs++; 99 | result = contents.call(data); 100 | if (typeof result === 'string') { 101 | this.indent(); 102 | text(this.esc(result)); 103 | if (data.format) { 104 | text('\n'); 105 | } 106 | } 107 | this.tabs--; 108 | return this.indent(); 109 | } 110 | }, 111 | render_tag: function(name, idclass, attrs, contents) { 112 | this.indent(); 113 | text("<" + name); 114 | if (idclass) { 115 | this.render_idclass(idclass); 116 | } 117 | if (attrs) { 118 | this.render_attrs(attrs); 119 | } 120 | if (__indexOf.call(this.self_closing, name) >= 0) { 121 | text(' />'); 122 | if (data.format) { 123 | text('\n'); 124 | } 125 | } else { 126 | text('>'); 127 | this.render_contents(contents); 128 | text("" + name + ">"); 129 | if (data.format) { 130 | text('\n'); 131 | } 132 | } 133 | return null; 134 | } 135 | }; 136 | tag = function() { 137 | var a, args, attrs, contents, idclass, name, _i, _len; 138 | name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 139 | for (_i = 0, _len = args.length; _i < _len; _i++) { 140 | a = args[_i]; 141 | switch (typeof a) { 142 | case 'function': 143 | contents = a; 144 | break; 145 | case 'object': 146 | attrs = a; 147 | break; 148 | case 'number': 149 | case 'boolean': 150 | contents = a; 151 | break; 152 | case 'string': 153 | if (args.length === 1) { 154 | contents = a; 155 | } else { 156 | if (a === args[0]) { 157 | idclass = a; 158 | } else { 159 | contents = a; 160 | } 161 | } 162 | } 163 | } 164 | return __ck.render_tag(name, idclass, attrs, contents); 165 | }; 166 | h = function(txt) { 167 | return String(txt).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); 168 | }; 169 | doctype = function(type) { 170 | if (type == null) { 171 | type = 'default'; 172 | } 173 | text(__ck.doctypes[type]); 174 | if (data.format) { 175 | return text('\n'); 176 | } 177 | }; 178 | text = function(txt) { 179 | __ck.buffer.push(String(txt)); 180 | return null; 181 | }; 182 | comment = function(cmt) { 183 | text(""); 184 | if (data.format) { 185 | return text('\n'); 186 | } 187 | }; 188 | coffeescript = function(param) { 189 | switch (typeof param) { 190 | case 'function': 191 | return script("" + __ck.coffeescript_helpers + "(" + param + ").call(this);"); 192 | case 'string': 193 | return script({ 194 | type: 'text/coffeescript' 195 | }, function() { 196 | return param; 197 | }); 198 | case 'object': 199 | param.type = 'text/coffeescript'; 200 | return script(param); 201 | } 202 | }; 203 | ie = function(condition, contents) { 204 | __ck.indent(); 205 | text(""); 208 | if (data.format) { 209 | return text('\n'); 210 | } 211 | }; 212 | __ck.doctypes = {"5":"","default":"","xml":"","transitional":"","strict":"","frameset":"","1.1":"","basic":"","mobile":"","ce":""};__ck.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; };";__ck.self_closing = ["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr","basefont","frame"];(function(){ul(function() { 213 | var guy, _i, _len, _ref, _results; 214 | _ref = this.stooges; 215 | _results = []; 216 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 217 | guy = _ref[_i]; 218 | _results.push(li(guy)); 219 | } 220 | return _results; 221 | });}).call(data);return __ck.buffer.join(''); 222 | }; 223 | }).call(this); -------------------------------------------------------------------------------- /examples/browser/regular/template.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | this.templates || (this.templates = {}); 3 | this.templates["template"] = function anonymous(data) { 4 | var a,i,li,p,s,th,u,ul;a = function(){return __ck.tag('a', arguments);};i = function(){return __ck.tag('i', arguments);};li = function(){return __ck.tag('li', arguments);};p = function(){return __ck.tag('p', arguments);};s = function(){return __ck.tag('s', arguments);};th = function(){return __ck.tag('th', arguments);};u = function(){return __ck.tag('u', arguments);};ul = function(){return __ck.tag('ul', arguments);};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; }; 5 | var coffeescript, comment, doctype, h, ie, tag, text, __ck, _ref, _ref2; 6 | if (data == null) { 7 | data = {}; 8 | } 9 | if ((_ref = data.format) != null) { 10 | _ref; 11 | } else { 12 | data.format = false; 13 | }; 14 | if ((_ref2 = data.autoescape) != null) { 15 | _ref2; 16 | } else { 17 | data.autoescape = false; 18 | }; 19 | __ck = { 20 | buffer: [], 21 | esc: function(txt) { 22 | if (data.autoescape) { 23 | return h(txt); 24 | } else { 25 | return String(txt); 26 | } 27 | }, 28 | tabs: 0, 29 | repeat: function(string, count) { 30 | return Array(count + 1).join(string); 31 | }, 32 | indent: function() { 33 | if (data.format) { 34 | return text(this.repeat(' ', this.tabs)); 35 | } 36 | }, 37 | tag: function(name, args) { 38 | var combo, i, _i, _len; 39 | combo = [name]; 40 | for (_i = 0, _len = args.length; _i < _len; _i++) { 41 | i = args[_i]; 42 | combo.push(i); 43 | } 44 | return tag.apply(data, combo); 45 | }, 46 | render_idclass: function(str) { 47 | var c, classes, i, id, _i, _j, _len, _len2, _ref3; 48 | classes = []; 49 | _ref3 = str.split('.'); 50 | for (_i = 0, _len = _ref3.length; _i < _len; _i++) { 51 | i = _ref3[_i]; 52 | if (i.indexOf('#') === 0) { 53 | id = i.replace('#', ''); 54 | } else { 55 | if (i !== '') { 56 | classes.push(i); 57 | } 58 | } 59 | } 60 | if (id) { 61 | text(" id=\"" + id + "\""); 62 | } 63 | if (classes.length > 0) { 64 | text(" class=\""); 65 | for (_j = 0, _len2 = classes.length; _j < _len2; _j++) { 66 | c = classes[_j]; 67 | if (c !== classes[0]) { 68 | text(' '); 69 | } 70 | text(c); 71 | } 72 | return text('"'); 73 | } 74 | }, 75 | render_attrs: function(obj) { 76 | var k, v, _results; 77 | _results = []; 78 | for (k in obj) { 79 | v = obj[k]; 80 | if (typeof v === 'boolean' && v) { 81 | v = k; 82 | } 83 | _results.push(v ? text(" " + k + "=\"" + (this.esc(v)) + "\"") : void 0); 84 | } 85 | return _results; 86 | }, 87 | render_contents: function(contents) { 88 | var result; 89 | switch (typeof contents) { 90 | case 'string': 91 | case 'number': 92 | case 'boolean': 93 | return text(this.esc(contents)); 94 | case 'function': 95 | if (data.format) { 96 | text('\n'); 97 | } 98 | this.tabs++; 99 | result = contents.call(data); 100 | if (typeof result === 'string') { 101 | this.indent(); 102 | text(this.esc(result)); 103 | if (data.format) { 104 | text('\n'); 105 | } 106 | } 107 | this.tabs--; 108 | return this.indent(); 109 | } 110 | }, 111 | render_tag: function(name, idclass, attrs, contents) { 112 | this.indent(); 113 | text("<" + name); 114 | if (idclass) { 115 | this.render_idclass(idclass); 116 | } 117 | if (attrs) { 118 | this.render_attrs(attrs); 119 | } 120 | if (__indexOf.call(this.self_closing, name) >= 0) { 121 | text(' />'); 122 | if (data.format) { 123 | text('\n'); 124 | } 125 | } else { 126 | text('>'); 127 | this.render_contents(contents); 128 | text("" + name + ">"); 129 | if (data.format) { 130 | text('\n'); 131 | } 132 | } 133 | return null; 134 | } 135 | }; 136 | tag = function() { 137 | var a, args, attrs, contents, idclass, name, _i, _len; 138 | name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 139 | for (_i = 0, _len = args.length; _i < _len; _i++) { 140 | a = args[_i]; 141 | switch (typeof a) { 142 | case 'function': 143 | contents = a; 144 | break; 145 | case 'object': 146 | attrs = a; 147 | break; 148 | case 'number': 149 | case 'boolean': 150 | contents = a; 151 | break; 152 | case 'string': 153 | if (args.length === 1) { 154 | contents = a; 155 | } else { 156 | if (a === args[0]) { 157 | idclass = a; 158 | } else { 159 | contents = a; 160 | } 161 | } 162 | } 163 | } 164 | return __ck.render_tag(name, idclass, attrs, contents); 165 | }; 166 | h = function(txt) { 167 | return String(txt).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); 168 | }; 169 | doctype = function(type) { 170 | if (type == null) { 171 | type = 'default'; 172 | } 173 | text(__ck.doctypes[type]); 174 | if (data.format) { 175 | return text('\n'); 176 | } 177 | }; 178 | text = function(txt) { 179 | __ck.buffer.push(String(txt)); 180 | return null; 181 | }; 182 | comment = function(cmt) { 183 | text(""); 184 | if (data.format) { 185 | return text('\n'); 186 | } 187 | }; 188 | coffeescript = function(param) { 189 | switch (typeof param) { 190 | case 'function': 191 | return script("" + __ck.coffeescript_helpers + "(" + param + ").call(this);"); 192 | case 'string': 193 | return script({ 194 | type: 'text/coffeescript' 195 | }, function() { 196 | return param; 197 | }); 198 | case 'object': 199 | param.type = 'text/coffeescript'; 200 | return script(param); 201 | } 202 | }; 203 | ie = function(condition, contents) { 204 | __ck.indent(); 205 | text(""); 208 | if (data.format) { 209 | return text('\n'); 210 | } 211 | }; 212 | __ck.doctypes = {"5":"","default":"","xml":"","transitional":"","strict":"","frameset":"","1.1":"","basic":"","mobile":"","ce":""};__ck.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; };";__ck.self_closing = ["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr","basefont","frame"];(function(){ul(function() { 213 | var guy, _i, _len, _ref, _results; 214 | _ref = this.stooges; 215 | _results = []; 216 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 217 | guy = _ref[_i]; 218 | _results.push(li(guy)); 219 | } 220 | return _results; 221 | });}).call(data);return __ck.buffer.join(''); 222 | }; 223 | }).call(this); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoffeeKup <☕/> 2 | ## Markup as CoffeeScript 3 | 4 | CoffeeKup is a templating engine for [node.js](http://nodejs.org) and browsers that lets you to write your HTML templates in 100% pure [CoffeeScript](http://coffeescript.org). 5 | 6 | It was created in celebration of [whyday](http://whyday.org/), as an application of the concept used in [Markaby](https://github.com/markaby/markaby) ("Markup as Ruby", by Tim Fletcher and why the lucky stiff) to CoffeeScript. 7 | 8 | Here's what a template written for CoffeeKup looks like: 9 | 10 | doctype 5 11 | html -> 12 | head -> 13 | meta charset: 'utf-8' 14 | title "#{@title or 'Untitled'} | A completely plausible website" 15 | meta(name: 'description', content: @description) if @description? 16 | 17 | link rel: 'stylesheet', href: '/css/app.css' 18 | 19 | style ''' 20 | body {font-family: sans-serif} 21 | header, nav, section, footer {display: block} 22 | ''' 23 | 24 | script src: '/js/jquery.js' 25 | 26 | coffeescript -> 27 | $(document).ready -> 28 | alert 'Alerts suck!' 29 | body -> 30 | header -> 31 | h1 @title or 'Untitled' 32 | 33 | nav -> 34 | ul -> 35 | (li -> a href: '/', -> 'Home') unless @path is '/' 36 | li -> a href: '/chunky', -> 'Bacon!' 37 | switch @user.role 38 | when 'owner', 'admin' 39 | li -> a href: '/admin', -> 'Secret Stuff' 40 | when 'vip' 41 | li -> a href: '/vip', -> 'Exclusive Stuff' 42 | else 43 | li -> a href: '/commoners', -> 'Just Stuff' 44 | 45 | div '#myid.myclass.anotherclass', style: 'position: fixed', -> 46 | p 'Divitis kills! Inline styling too.' 47 | 48 | section -> 49 | # A helper function you built and included. 50 | breadcrumb separator: '>', clickable: yes 51 | 52 | h2 "Let's count to 10:" 53 | p i for i in [1..10] 54 | 55 | # Another hypothetical helper. 56 | form_to @post, -> 57 | textbox '#title', label: 'Title:' 58 | textbox '#author', label: 'Author:' 59 | submit 'Save' 60 | 61 | footer -> 62 | # CoffeeScript comments. Not visible in the output document. 63 | comment 'HTML comments.' 64 | p 'Bye!' 65 | 66 | Interactive demo at [coffeekup.org](http://coffeekup.org). 67 | 68 | ## _why? 69 | 70 | - **One language to rule them all**. JavaScript is everywhere, thus so is CoffeeScript. Servers, browsers, even databases. If extending this to rendering logic and UI structure (server and client side) is desirable to you, CoffeeKup is your friend. 71 | 72 | - **More specifically, one _outstanding_ language**. CoffeeScript is one hell of a clean, expressive, flexible and powerful language. It's hard to find such combination, especially if you need it to run in the browser too. 73 | 74 | - **Not yet another specialized language to learn**. Transferable knowledge FTW. 75 | 76 | - **Embed your templates in CoffeeScript nicely**. Templates are just functions, so they don't lose syntax highlighting and syntax checking when embedded in CoffeeScript apps. 77 | 78 | - **Embed CoffeeScript in your templates nicely**. In the same manner, you can write the contents of ` 188 | 191 | 192 | This is one of many browser deployment possibilities, pre-compiling your template on the server to a standalone function. To see all serving suggestions, check out [regular](http://github.com/mauricemach/coffeekup/blob/master/examples/browser/regular/index.html), [decaf](http://github.com/mauricemach/coffeekup/blob/master/examples/browser/decaf/index.html) and [crème](http://github.com/mauricemach/coffeekup/blob/master/examples/browser/creme/index.html). 193 | 194 | Command-line: 195 | 196 | $ coffeekup -h 197 | 198 | Usage: 199 | coffeekup [options] path/to/template.coffee 200 | 201 | --js compile template to js function 202 | -n, --namespace global object holding the templates (default: "templates") 203 | -w, --watch watch templates for changes, and recompile 204 | -o, --output set the directory for compiled html 205 | -p, --print print the compiled html to stdout 206 | -f, --format apply line breaks and indentation to html output 207 | -u, --utils add helper locals (currently only "render") 208 | -v, --version display CoffeeKup version 209 | -h, --help display this help message 210 | 211 | See [/examples](http://github.com/mauricemach/coffeekup/tree/master/examples) for complete versions (you have to run `cake build` first). 212 | 213 | Please note that even though all examples are given in CoffeeScript, you can also use their plain JavaScript counterparts just fine. 214 | 215 | ## Resources 216 | 217 | - [API reference](https://github.com/mauricemach/coffeekup/blob/master/docs/reference.md) 218 | 219 | - [Mailing list](https://groups.google.com/group/coffeekup) 220 | 221 | - [Issues](https://github.com/mauricemach/coffeekup/issues) 222 | 223 | - **IRC**: #coffeekup on irc.freenode.net 224 | 225 | - [A Beginners's Introduction to CoffeeKup](https://github.com/mark-hahn/coffeekup-intro) 226 | 227 | ## Tools 228 | 229 | - [html2coffeekup](https://github.com/brandonbloom/html2coffeekup) - Converts HTML to CoffeeKup templates. 230 | 231 | - [htmlkup](https://github.com/colinta/htmlkup) - Another HTML converter, stdin/stdout based. 232 | 233 | - [ice](https://github.com/ludicast/ice) - CoffeeKup and Eco in Rails ([screencast](http://vimeo.com/25907220)). 234 | 235 | - [coffee-world](https://github.com/khoomeister/coffee-world) - Tool to watch and compile HTML with CoffeeKup, CSS with coffee-css and JS with CoffeeScript. 236 | 237 | - [cupcake](https://github.com/twilson63/cupcake) - Express app generator with CoffeeKup support. 238 | 239 | ## Related projects 240 | 241 | - [ck](https://github.com/aeosynth/ck) - "a smaller, faster coffeekup": Alternative, barebones implementation. 242 | 243 | - [ckup](https://github.com/satyr/ckup) - "Markup as Coco": Similar engine but for [Coco](https://github.com/satyr/coco) ("Unfancy CoffeeScript"). 244 | 245 | - [Eco](https://github.com/sstephenson/eco) - "Embedded CoffeeScript templates": "EJS/ERB" for CoffeeScript. 246 | 247 | - [timbits](https://github.com/Postmedia/timbits) - "Widget framework based on Express and CoffeeScript". 248 | 249 | - [coffee-css](https://github.com/khoomeister/coffee-css) - "More CSS for CoffeeScript". 250 | 251 | - [ccss](https://github.com/aeosynth/ccss) - "CoffeeScript CSS". 252 | 253 | ## Compatibility 254 | 255 | Latest version tested with node 0.4.9 and CoffeeScript 1.1.1. 256 | 257 | ## Special thanks 258 | 259 | - [Jeremy Ashkenas](https://github.com/jashkenas), for the amazing CoffeeScript language. 260 | - [why the lucky stiff](Why_the_lucky_stiff), for the inspiration. 261 | -------------------------------------------------------------------------------- /src/coffeekup.coffee: -------------------------------------------------------------------------------- 1 | # **CoffeeKup** 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 | 12 | if window? 13 | coffeekup = window.CoffeeKup = {} 14 | coffee = if CoffeeScript? then CoffeeScript else null 15 | else 16 | coffeekup = exports 17 | coffee = require 'coffee-script' 18 | 19 | coffeekup.version = '0.3.1' 20 | 21 | # Values available to the `doctype` function inside a template. 22 | # Ex.: `doctype 'strict'` 23 | coffeekup.doctypes = 24 | 'default': '' 25 | '5': '' 26 | 'xml': '' 27 | 'transitional': '' 28 | 'strict': '' 29 | 'frameset': '' 30 | '1.1': '', 31 | 'basic': '' 32 | 'mobile': '' 33 | 'ce': '' 34 | 35 | # CoffeeScript-generated JavaScript may contain anyone of these; but when we 36 | # take a function to string form to manipulate it, and then recreate it through 37 | # the `Function()` constructor, it loses access to its parent scope and 38 | # consequently to any helpers it might need. So we need to reintroduce these 39 | # inside any "rewritten" function. 40 | coffeescript_helpers = """ 41 | var __slice = Array.prototype.slice; 42 | var __hasProp = Object.prototype.hasOwnProperty; 43 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 44 | var __extends = function(child, parent) { 45 | for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } 46 | function ctor() { this.constructor = child; } 47 | ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; 48 | return child; }; 49 | var __indexOf = Array.prototype.indexOf || function(item) { 50 | for (var i = 0, l = this.length; i < l; i++) { 51 | if (this[i] === item) return i; 52 | } return -1; }; 53 | """.replace /\n/g, '' 54 | 55 | # Private HTML element reference. 56 | # Please mind the gap (1 space at the beginning of each subsequent line). 57 | elements = 58 | # Valid HTML 5 elements requiring a closing tag. 59 | # Note: the `var` element is out for obvious reasons, please use `tag 'var'`. 60 | regular: 'a abbr address article aside audio b bdi bdo blockquote body button 61 | canvas caption cite code colgroup datalist dd del details dfn div dl dt em 62 | fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup 63 | html i iframe ins kbd label legend li map mark menu meter nav noscript object 64 | ol optgroup option output p pre progress q rp rt ruby s samp script section 65 | select small span strong style sub summary sup table tbody td textarea tfoot 66 | th thead time title tr u ul video' 67 | 68 | # Valid self-closing HTML 5 elements. 69 | void: 'area base br col command embed hr img input keygen link meta param 70 | source track wbr' 71 | 72 | obsolete: 'applet acronym bgsound dir frameset noframes isindex listing 73 | nextid noembed plaintext rb strike xmp big blink center font marquee multicol 74 | nobr spacer tt' 75 | 76 | obsolete_void: 'basefont frame' 77 | 78 | # Create a unique list of element names merging the desired groups. 79 | merge_elements = (args...) -> 80 | result = [] 81 | for a in args 82 | for element in elements[a].split ' ' 83 | result.push element unless element in result 84 | result 85 | 86 | # Public/customizable list of possible elements. 87 | # For each name in this list that is also present in the input template code, 88 | # a function with the same name will be added to the compiled template. 89 | coffeekup.tags = merge_elements 'regular', 'obsolete', 'void', 'obsolete_void' 90 | 91 | # Public/customizable list of elements that should be rendered self-closed. 92 | coffeekup.self_closing = merge_elements 'void', 'obsolete_void' 93 | 94 | # This is the basic material from which compiled templates will be formed. 95 | # It will be manipulated in its string form at the `coffeekup.compile` function 96 | # to generate the final template function. 97 | skeleton = (data = {}) -> 98 | # Whether to generate formatted HTML with indentation and line breaks, or 99 | # just the natural "faux-minified" output. 100 | data.format ?= off 101 | 102 | # Whether to autoescape all content or let you handle it on a case by case 103 | # basis with the `h` function. 104 | data.autoescape ?= off 105 | 106 | # Internal CoffeeKup stuff. 107 | __ck = 108 | buffer: [] 109 | 110 | esc: (txt) -> 111 | if data.autoescape then h(txt) else String(txt) 112 | 113 | tabs: 0 114 | 115 | repeat: (string, count) -> Array(count + 1).join string 116 | 117 | indent: -> text @repeat(' ', @tabs) if data.format 118 | 119 | # Adapter to keep the builtin tag functions DRY. 120 | tag: (name, args) -> 121 | combo = [name] 122 | combo.push i for i in args 123 | tag.apply data, combo 124 | 125 | render_idclass: (str) -> 126 | classes = [] 127 | 128 | for i in str.split '.' 129 | if '#' in i 130 | id = i.replace '#', '' 131 | else 132 | classes.push i unless i is '' 133 | 134 | text " id=\"#{id}\"" if id 135 | 136 | if classes.length > 0 137 | text " class=\"" 138 | for c in classes 139 | text ' ' unless c is classes[0] 140 | text c 141 | text '"' 142 | 143 | render_attrs: (obj, prefix = '') -> 144 | for k, v of obj 145 | # `true` is rendered as `selected="selected"`. 146 | v = k if typeof v is 'boolean' and v 147 | 148 | # Functions are rendered in an executable form. 149 | v = "(#{v}).call(this);" if typeof v is 'function' 150 | 151 | # Prefixed attribute. 152 | if typeof v is 'object' and v not instanceof Array 153 | # `data: {icon: 'foo'}` is rendered as `data-icon="foo"`. 154 | @render_attrs(v, prefix + k + '-') 155 | # `undefined`, `false` and `null` result in the attribute not being rendered. 156 | else if v 157 | # strings, numbers, arrays and functions are rendered "as is". 158 | text " #{prefix + k}=\"#{@esc(v)}\"" 159 | 160 | render_contents: (contents, safe) -> 161 | safe ?= false 162 | switch typeof contents 163 | when 'string', 'number', 'boolean' 164 | text if safe then contents else @esc(contents) 165 | when 'function' 166 | text '\n' if data.format 167 | @tabs++ 168 | result = contents.call data 169 | if typeof result is 'string' 170 | @indent() 171 | text if safe then result else @esc(result) 172 | text '\n' if data.format 173 | @tabs-- 174 | @indent() 175 | 176 | render_tag: (name, idclass, attrs, contents) -> 177 | @indent() 178 | 179 | text "<#{name}" 180 | @render_idclass(idclass) if idclass 181 | @render_attrs(attrs) if attrs 182 | 183 | if name in @self_closing 184 | text ' />' 185 | text '\n' if data.format 186 | else 187 | text '>' 188 | 189 | @render_contents(contents) 190 | 191 | text "#{name}>" 192 | text '\n' if data.format 193 | 194 | null 195 | 196 | tag = (name, args...) -> 197 | for a in args 198 | switch typeof a 199 | when 'function' 200 | contents = a 201 | when 'object' 202 | attrs = a 203 | when 'number', 'boolean' 204 | contents = a 205 | when 'string' 206 | if args.length is 1 207 | contents = a 208 | else 209 | if a is args[0] 210 | idclass = a 211 | else 212 | contents = a 213 | 214 | __ck.render_tag(name, idclass, attrs, contents) 215 | 216 | yield = (f) -> 217 | temp_buffer = [] 218 | old_buffer = __ck.buffer 219 | __ck.buffer = temp_buffer 220 | f() 221 | __ck.buffer = old_buffer 222 | temp_buffer.join '' 223 | 224 | h = (txt) -> 225 | String(txt).replace(/&/g, '&') 226 | .replace(//g, '>') 228 | .replace(/"/g, '"') 229 | 230 | doctype = (type = 'default') -> 231 | text __ck.doctypes[type] 232 | text '\n' if data.format 233 | 234 | safe = (val) -> 235 | __ck.render_contents(val, true) 236 | 237 | text = (txt) -> 238 | __ck.buffer.push String(txt) 239 | null 240 | 241 | comment = (cmt) -> 242 | text "" 243 | text '\n' if data.format 244 | 245 | coffeescript = (param) -> 246 | switch typeof param 247 | # `coffeescript -> alert 'hi'` becomes: 248 | # `` 249 | when 'function' 250 | script "#{__ck.coffeescript_helpers}(#{param}).call(this);" 251 | # `coffeescript "alert 'hi'"` becomes: 252 | # `` 253 | when 'string' 254 | script type: 'text/coffeescript', -> param 255 | # `coffeescript src: 'script.coffee'` becomes: 256 | # `` 257 | when 'object' 258 | param.type = 'text/coffeescript' 259 | script param 260 | 261 | # Conditional IE comments. 262 | ie = (condition, contents) -> 263 | __ck.indent() 264 | 265 | text "" 268 | text '\n' if data.format 269 | 270 | null 271 | 272 | # Stringify the skeleton and unwrap it from its enclosing `function(){}`, then 273 | # add the CoffeeScript helpers. 274 | skeleton = String(skeleton) 275 | .replace(/function\s*\(.*\)\s*\{/, '') 276 | .replace(/return null;\s*\}$/, '') 277 | 278 | skeleton = coffeescript_helpers + skeleton 279 | 280 | # Compiles a template into a standalone JavaScript function. 281 | coffeekup.compile = (template, options = {}) -> 282 | # The template can be provided as either a function or a CoffeeScript string 283 | # (in the latter case, the CoffeeScript compiler must be available). 284 | if typeof template is 'function' then template = String(template) 285 | else if typeof template is 'string' and coffee? 286 | template = coffee.compile template, bare: yes 287 | template = "function(){#{template}}" 288 | 289 | # If an object `hardcode` is provided, insert the stringified value 290 | # of each variable directly in the function body. This is a less flexible but 291 | # faster alternative to the standard method of using `with` (see below). 292 | hardcoded_locals = '' 293 | 294 | if options.hardcode 295 | for k, v of options.hardcode 296 | if typeof v is 'function' 297 | # Make sure these functions have access to `data` as `@/this`. 298 | hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};" 299 | else hardcoded_locals += "var #{k} = #{JSON.stringify v};" 300 | 301 | # Add a function for each tag this template references. We don't want to have 302 | # all hundred-odd tags wasting space in the compiled function. 303 | tag_functions = '' 304 | tags_used = [] 305 | 306 | for t in coffeekup.tags 307 | if template.indexOf(t) > -1 or hardcoded_locals.indexOf(t) > -1 308 | tags_used.push t 309 | 310 | tag_functions += "var #{tags_used.join ','};" 311 | for t in tags_used 312 | tag_functions += "#{t} = function(){return __ck.tag('#{t}', arguments);};" 313 | 314 | # Main function assembly. 315 | code = tag_functions + hardcoded_locals + skeleton 316 | 317 | code += "__ck.doctypes = #{JSON.stringify coffeekup.doctypes};" 318 | code += "__ck.coffeescript_helpers = #{JSON.stringify coffeescript_helpers};" 319 | code += "__ck.self_closing = #{JSON.stringify coffeekup.self_closing};" 320 | 321 | # If `locals` is set, wrap the template inside a `with` block. This is the 322 | # most flexible but slower approach to specifying local variables. 323 | code += 'with(data.locals){' if options.locals 324 | code += "(#{template}).call(data);" 325 | code += '}' if options.locals 326 | code += "return __ck.buffer.join('');" 327 | 328 | new Function('data', code) 329 | 330 | cache = {} 331 | 332 | # Template in, HTML out. Accepts functions or strings as does `coffeekup.compile`. 333 | # 334 | # Accepts an option `cache`, by default `false`. If set to `false` templates will 335 | # be recompiled each time. 336 | # 337 | # `options` is just a convenience parameter to pass options separately from the 338 | # data, but the two will be merged and passed down to the compiler (which uses 339 | # `locals` and `hardcode`), and the template (which understands `locals`, `format` 340 | # and `autoescape`). 341 | coffeekup.render = (template, data = {}, options = {}) -> 342 | data[k] = v for k, v of options 343 | data.cache ?= off 344 | 345 | if data.cache and cache[template]? then tpl = cache[template] 346 | else if data.cache then tpl = cache[template] = coffeekup.compile(template, data) 347 | else tpl = coffeekup.compile(template, data) 348 | tpl(data) 349 | 350 | unless window? 351 | coffeekup.adapters = 352 | # Legacy adapters for when CoffeeKup expected data in the `context` attribute. 353 | simple: coffeekup.render 354 | meryl: coffeekup.render 355 | 356 | express: 357 | TemplateError: class extends Error 358 | constructor: (@message) -> 359 | Error.call this, @message 360 | Error.captureStackTrace this, arguments.callee 361 | name: 'TemplateError' 362 | 363 | compile: (template, data) -> 364 | # Allows `partial 'foo'` instead of `text @partial 'foo'`. 365 | data.hardcode ?= {} 366 | data.hardcode.partial = -> 367 | text @partial.apply @, arguments 368 | 369 | TemplateError = @TemplateError 370 | try tpl = coffeekup.compile(template, data) 371 | catch e then throw new TemplateError "Error compiling #{data.filename}: #{e.message}" 372 | 373 | return -> 374 | try tpl arguments... 375 | catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}" -------------------------------------------------------------------------------- /docs/coffeekup.html: -------------------------------------------------------------------------------- 1 | coffeekup.coffee | |
|---|---|
| CoffeeKup lets you to write HTML templates in 100% pure 2 | CoffeeScript. 3 | 4 |You can run it on node.js 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 by Tim Fletcher and why the lucky 10 | stiff. | if window?
11 | coffeekup = window.CoffeeKup = {}
12 | coffee = if CoffeeScript? then CoffeeScript else null
13 | else
14 | coffeekup = exports
15 | coffee = require 'coffee-script'
16 |
17 | coffeekup.version = '0.3.1' |
| Values available to the | coffeekup.doctypes =
19 | 'default': '<!DOCTYPE html>'
20 | '5': '<!DOCTYPE html>'
21 | 'xml': '<?xml version="1.0" encoding="utf-8" ?>'
22 | 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
23 | 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
24 | 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
25 | '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
26 | 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
27 | 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
28 | 'ce': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "ce-html-1.0-transitional.dtd">' |
| CoffeeScript-generated JavaScript may contain anyone of these; but when we
29 | take a function to string form to manipulate it, and then recreate it through
30 | the | coffeescript_helpers = """
33 | var __slice = Array.prototype.slice;
34 | var __hasProp = Object.prototype.hasOwnProperty;
35 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
36 | var __extends = function(child, parent) {
37 | for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
38 | function ctor() { this.constructor = child; }
39 | ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype;
40 | return child; };
41 | var __indexOf = Array.prototype.indexOf || function(item) {
42 | for (var i = 0, l = this.length; i < l; i++) {
43 | if (this[i] === item) return i;
44 | } return -1; };
45 | """.replace /\n/g, '' |
| Private HTML element reference. 46 | Please mind the gap (1 space at the beginning of each subsequent line). | elements = |
| Valid HTML 5 elements requiring a closing tag.
47 | Note: the | regular: 'a abbr address article aside audio b bdi bdo blockquote body button
48 | canvas caption cite code colgroup datalist dd del details dfn div dl dt em
49 | fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
50 | html i iframe ins kbd label legend li map mark menu meter nav noscript object
51 | ol optgroup option output p pre progress q rp rt ruby s samp script section
52 | select small span strong style sub summary sup table tbody td textarea tfoot
53 | 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
54 | source track wbr'
55 |
56 | obsolete: 'applet acronym bgsound dir frameset noframes isindex listing
57 | nextid noembed plaintext rb strike xmp big blink center font marquee multicol
58 | nobr spacer tt'
59 |
60 | obsolete_void: 'basefont frame' |
| Create a unique list of element names merging the desired groups. | merge_elements = (args...) ->
61 | result = []
62 | for a in args
63 | for element in elements[a].split ' '
64 | result.push element unless element in result
65 | result |
| Public/customizable list of possible elements. 66 | For each name in this list that is also present in the input template code, 67 | 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.
68 | It will be manipulated in its string form at the | skeleton = (data = {}) -> |
| Whether to generate formatted HTML with indentation and line breaks, or 70 | just the natural "faux-minified" output. | data.format ?= off |
| Whether to autoescape all content or let you handle it on a case by case
71 | basis with the | data.autoescape ?= off |
| Internal CoffeeKup stuff. | __ck =
72 | buffer: []
73 |
74 | esc: (txt) ->
75 | if data.autoescape then h(txt) else String(txt)
76 |
77 | tabs: 0
78 |
79 | repeat: (string, count) -> Array(count + 1).join string
80 |
81 | indent: -> text @repeat(' ', @tabs) if data.format |
| Adapter to keep the builtin tag functions DRY. | tag: (name, args) ->
82 | combo = [name]
83 | combo.push i for i in args
84 | tag.apply data, combo
85 |
86 | render_idclass: (str) ->
87 | classes = []
88 |
89 | for i in str.split '.'
90 | if '#' in i
91 | id = i.replace '#', ''
92 | else
93 | classes.push i unless i is ''
94 |
95 | text " id=\"#{id}\"" if id
96 |
97 | if classes.length > 0
98 | text " class=\""
99 | for c in classes
100 | text ' ' unless c is classes[0]
101 | text c
102 | text '"'
103 |
104 | render_attrs: (obj, prefix = '') ->
105 | for k, v of obj |
|
| v = k if typeof v is 'boolean' and v
106 | |
| 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 |
|
| @render_attrs(v, prefix + k + '-') |
|
| else if v |
| strings, numbers, arrays and functions are rendered "as is". | text " #{prefix + k}=\"#{@esc(v)}\""
107 |
108 | render_contents: (contents) ->
109 | switch typeof contents
110 | when 'string', 'number', 'boolean'
111 | text @esc(contents)
112 | when 'function'
113 | text '\n' if data.format
114 | @tabs++
115 | result = contents.call data
116 | if typeof result is 'string'
117 | @indent()
118 | text @esc(result)
119 | text '\n' if data.format
120 | @tabs--
121 | @indent()
122 |
123 | render_tag: (name, idclass, attrs, contents) ->
124 | @indent()
125 |
126 | text "<#{name}"
127 | @render_idclass(idclass) if idclass
128 | @render_attrs(attrs) if attrs
129 |
130 | if name in @self_closing
131 | text ' />'
132 | text '\n' if data.format
133 | else
134 | text '>'
135 |
136 | @render_contents(contents)
137 |
138 | text "</#{name}>"
139 | text '\n' if data.format
140 |
141 | null
142 |
143 | tag = (name, args...) ->
144 | for a in args
145 | switch typeof a
146 | when 'function'
147 | contents = a
148 | when 'object'
149 | attrs = a
150 | when 'number', 'boolean'
151 | contents = a
152 | when 'string'
153 | if args.length is 1
154 | contents = a
155 | else
156 | if a is args[0]
157 | idclass = a
158 | else
159 | contents = a
160 |
161 | __ck.render_tag(name, idclass, attrs, contents)
162 |
163 | yield = (f) ->
164 | temp_buffer = []
165 | old_buffer = __ck.buffer
166 | __ck.buffer = temp_buffer
167 | f()
168 | __ck.buffer = old_buffer
169 | temp_buffer.join ''
170 |
171 | h = (txt) ->
172 | String(txt).replace(/&/g, '&')
173 | .replace(/</g, '<')
174 | .replace(/>/g, '>')
175 | .replace(/"/g, '"')
176 |
177 | doctype = (type = 'default') ->
178 | text __ck.doctypes[type]
179 | text '\n' if data.format
180 |
181 | text = (txt) ->
182 | __ck.buffer.push String(txt)
183 | null
184 |
185 | comment = (cmt) ->
186 | text "<!--#{cmt}-->"
187 | text '\n' if data.format
188 |
189 | coffeescript = (param) ->
190 | switch typeof param |
|
| when 'function'
192 | script "#{__ck.coffeescript_helpers}(#{param}).call(this);" |
|
| when 'string'
194 | script type: 'text/coffeescript', -> param |
|
| when 'object'
196 | param.type = 'text/coffeescript'
197 | script param
198 | |
| Conditional IE comments. | ie = (condition, contents) ->
199 | __ck.indent()
200 |
201 | text "<!--[if #{condition}]>"
202 | __ck.render_contents(contents)
203 | text "<![endif]-->"
204 | text '\n' if data.format
205 |
206 | null |
| Stringify the skeleton and unwrap it from its enclosing | skeleton = String(skeleton)
208 | .replace(/function\s*\(.*\)\s*\{/, '')
209 | .replace(/return null;\s*\}$/, '')
210 |
211 | 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 212 | (in the latter case, the CoffeeScript compiler must be available). | if typeof template is 'function' then template = String(template)
213 | else if typeof template is 'string' and coffee?
214 | template = coffee.compile template, bare: yes
215 | template = "function(){#{template}}" |
| If an object | hardcoded_locals = ''
218 |
219 | if options.hardcode
220 | for k, v of options.hardcode
221 | if typeof v is 'function' |
| Make sure these functions have access to | hardcoded_locals += "var #{k} = function(){return (#{v}).apply(data, arguments);};"
222 | else hardcoded_locals += "var #{k} = #{JSON.stringify v};" |
| Add a function for each tag this template references. We don't want to have 223 | all hundred-odd tags wasting space in the compiled function. | tag_functions = ''
224 | tags_used = []
225 |
226 | for t in coffeekup.tags
227 | if template.indexOf(t) > -1 or hardcoded_locals.indexOf(t) > -1
228 | tags_used.push t
229 |
230 | tag_functions += "var #{tags_used.join ','};"
231 | for t in tags_used
232 | tag_functions += "#{t} = function(){return __ck.tag('#{t}', arguments);};" |
| Main function assembly. | code = tag_functions + hardcoded_locals + skeleton
233 |
234 | code += "__ck.doctypes = #{JSON.stringify coffeekup.doctypes};"
235 | code += "__ck.coffeescript_helpers = #{JSON.stringify coffeescript_helpers};"
236 | code += "__ck.self_closing = #{JSON.stringify coffeekup.self_closing};" |
| If | code += 'with(data.locals){' if options.locals
238 | code += "(#{template}).call(data);"
239 | code += '}' if options.locals
240 | code += "return __ck.buffer.join('');"
241 |
242 | new Function('data', code)
243 |
244 | cache = {} |
| Template in, HTML out. Accepts functions or strings as does Accepts an option
| coffeekup.render = (template, data = {}, options = {}) ->
253 | data[k] = v for k, v of options
254 | data.cache ?= off
255 |
256 | if data.cache and cache[template]? then tpl = cache[template]
257 | else if data.cache then tpl = cache[template] = coffeekup.compile(template, data)
258 | else tpl = coffeekup.compile(template, data)
259 | tpl(data)
260 |
261 | unless window?
262 | coffeekup.adapters = |
| Legacy adapters for when CoffeeKup expected data in the | simple: coffeekup.render
263 | meryl: coffeekup.render
264 |
265 | express:
266 | TemplateError: class extends Error
267 | constructor: (@message) ->
268 | Error.call this, @message
269 | Error.captureStackTrace this, arguments.callee
270 | name: 'TemplateError'
271 |
272 | compile: (template, data) -> |
| Allows | data.hardcode ?= {}
273 | data.hardcode.partial = ->
274 | text @partial.apply @, arguments
275 |
276 | TemplateError = @TemplateError
277 | try tpl = coffeekup.compile(template, data)
278 | catch e then throw new TemplateError "Error compiling #{data.filename}: #{e.message}"
279 |
280 | return ->
281 | try tpl arguments...
282 | catch e then throw new TemplateError "Error rendering #{data.filename}: #{e.message}"
283 |
284 | |