├── tests ├── index.html ├── index.js ├── common.js ├── test-route.js ├── test-hostrouter.js ├── test-content-types.js ├── run.js ├── test-notfound.js └── test-basic.js ├── package.json ├── rfc822.js ├── README.mkd ├── index.js └── handlebars.js /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var tako = require('../index') 2 | 3 | var t = tako() 4 | console.error(t.route) 5 | t.httpServer.listen(9999, function () { 6 | console.log('open http://localhost:9999') 7 | }) 8 | t.route('/').file('./index.html') 9 | 10 | 11 | t.sockets.on('connection', function (socket) { 12 | t.sockets.emit('news', { will: 'be received by everyone'}); 13 | 14 | socket.on('disconnect', function () { 15 | t.sockets.emit('user disconnected'); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /tests/common.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | 3 | exports.PORT = 6000 4 | exports.URL = 'http://localhost:' + exports.PORT 5 | 6 | exports.all = function (functions, cb) { 7 | var done = functions.length 8 | functions.forEach(function (f) { 9 | f[0](function () { 10 | f[1].apply(this, arguments) 11 | if (--done === 0) return cb() 12 | }) 13 | }) 14 | } 15 | 16 | exports.assertJSON = function (res) { 17 | assert.equal(res.headers['content-type'], 'application/json') 18 | } 19 | 20 | exports.assertStatus = function (res, status) { 21 | assert.equal(res.statusCode, status) 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "tako" 2 | , "description" : "Functional web framework." 3 | , "tags" : ["http", "simple", "util", "utility", "web", "framework", "webframework"] 4 | , "version" : "0.3.0" 5 | , "author" : "Mikeal Rogers6 | npm install tako 7 |8 | 9 | ## Add Sockets 10 | 11 |
12 | npm install socket.io 13 |14 | 15 | Or from source: 16 | 17 |
18 | git clone git://github.com/mikeal/tako.git 19 | cd tako 20 | npm link 21 |22 | 23 | ## Usage 24 | 25 | ```javascript 26 | var tako = require('tako') 27 | , request = require('request') 28 | , path = require('path') 29 | , app = tako() 30 | ; 31 | 32 | app.route('/static/*').files(path.join(__dirname, 'static')) 33 | 34 | app.route('/proxypass', function (req, resp) { 35 | req.pipe(request("http://otherserver.com"+req.url).pipe(resp)) 36 | }) 37 | 38 | app.route('/hello.json').json({msg:'hello!'}) 39 | 40 | app.route('/plaintext').text('I like text/plain') 41 | 42 | app.route('/') 43 | .html(function (req, resp) { 44 | request('http://me.iriscouch.com/db', {json:true}, function (e, r) { 45 | if (e) return resp.error(e) 46 | if (r.statusCode !== 200) return resp.error(r) 47 | resp.end('cool'+r.body.index+'') 48 | }) 49 | }) 50 | .methods('GET') 51 | ; 52 | 53 | // Ported example from socket.io docs to show integration 54 | app.sockets.on('connection', function (socket) { 55 | app.sockets.emit('news', { will: 'be received by everyone'}); 56 | socket.on('disconnect', function () { 57 | app.sockets.emit('user disconnected') 58 | }) 59 | }) 60 | 61 | app.httpServer.listen(80) 62 | app.httpsServer.listen(443) 63 | ``` 64 | 65 | ### Routing multiple domains 66 | 67 | ```javascript 68 | var tako = require('../index') 69 | , app1 = tako() 70 | , app2 = tako() 71 | , default = tako() 72 | , router = tako.router() 73 | ; 74 | 75 | app1.route('/name').text('app1') 76 | app2.route('/name').text('app2') 77 | default.route('/name').text('default') 78 | 79 | router.host('app1.localhost', app1) 80 | router.host('app2.localhost', app2) 81 | router.default(default) 82 | 83 | router.httpServer.listen(80) 84 | router.httpsServer.listen(443) 85 | ``` 86 | -------------------------------------------------------------------------------- /tests/test-notfound.js: -------------------------------------------------------------------------------- 1 | var tako = require('../index') 2 | , request = require('request') 3 | , assert = require('assert') 4 | , path = require('path') 5 | , fs = require('fs') 6 | , app = tako() 7 | , port = 8080 8 | , url = 'http://localhost:' + port + '/' 9 | , index = fs.readFileSync(path.join(__dirname, 'index.html')).toString() 10 | 11 | app.route('/dir/*').files(__dirname) 12 | app.httpServer.listen(8080, function () { 13 | // Test default 404 14 | request.get(url+'nope', function (e, resp, body) { 15 | if (e) throw e 16 | assert.equal(resp.statusCode, 404) 17 | assert.equal(body, 'Not Found') 18 | 19 | // Make sure file serving works 20 | request.get(url+'dir/index.html', function (e, resp, body) { 21 | if (e) throw e 22 | assert.equal(resp.statusCode, 200) 23 | assert.equal(resp.headers['content-type'], 'text/html') 24 | assert.equal(body, index) 25 | 26 | // Set default 404 27 | app.notfound(path.join(__dirname, 'index.html')) 28 | 29 | // Test unfound route returns new default 404 30 | request.get(url+'nope', function (e, resp, body) { 31 | if (e) throw e 32 | assert.equal(resp.statusCode, 404) 33 | assert.equal(body, index) 34 | 35 | // Test that unfound files return new default 36 | request.get(url+'dir/nothing.txt', function (e, resp, body) { 37 | if (e) throw e 38 | assert.equal(resp.statusCode, 404) 39 | assert.equal(body, index) 40 | 41 | // Test notfound JSON 42 | app.notfound({test:'asdf'}) 43 | request.get(url+'nope', {json:true}, function (e, resp, body) { 44 | if (e) throw e 45 | assert.equal(resp.statusCode, 404) 46 | assert.equal(resp.headers['content-type'], 'application/json') 47 | assert.deepEqual(body, {test:'asdf'}) 48 | 49 | // Test function handler 50 | app.notfound(function (req, resp) {resp.statusCode = 404; resp.end('asdf')}) 51 | request.get(url+'nope', function (e, resp, body) { 52 | if (e) throw e 53 | assert.equal(resp.statusCode, 404) 54 | assert.equal(body, 'asdf') 55 | console.log('All tests passed') 56 | process.exit() 57 | }) 58 | }) 59 | }) 60 | }) 61 | }) 62 | }) 63 | }) -------------------------------------------------------------------------------- /tests/test-basic.js: -------------------------------------------------------------------------------- 1 | var tako = require('../index') 2 | , request = require('request') 3 | , assert = require('assert') 4 | , fs = require('fs') 5 | ; 6 | 7 | var t = tako() 8 | t 9 | .route('/') 10 | .json(function (req, resp) { 11 | resp.end({text:'hello world'}) 12 | }) 13 | .html(function (req, resp) { 14 | resp.end('Hello World') 15 | }) 16 | .on('request', function (req, resp) { 17 | resp.statusCode = 200 18 | resp.setHeader('content-type', 'text/plain') 19 | resp.end('hello') 20 | }) 21 | 22 | t 23 | .route('/static') 24 | .json({text:'hello world'}) 25 | .html('Hello World') 26 | 27 | t 28 | .route('/puts') 29 | .json(function (req, resp) { 30 | req.on('json', function (obj) { 31 | resp.statusCode = 201 32 | resp.end(obj) 33 | }) 34 | }) 35 | 36 | t 37 | .route('/file/js') 38 | .file(__filename) 39 | 40 | t 41 | .route('/files/*') 42 | .files(__dirname) 43 | 44 | t.route('/buffer').html(new Buffer('Hello World')) 45 | 46 | 47 | var url = 'http://localhost:8000/' 48 | 49 | counter = 0 50 | 51 | function end () { 52 | counter-- 53 | if (counter === 0) t.close() 54 | } 55 | 56 | t.httpServer.listen(8000, function () { 57 | counter++ 58 | request({url:url,headers:{'accept':'application/json'}}, function (e, resp, body) { 59 | if (e) throw e 60 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 61 | assert.equal(resp.headers['content-type'], 'application/json') 62 | assert.equal(body, JSON.stringify({text:'hello world'})) 63 | console.log('Passed json /') 64 | end() 65 | }) 66 | 67 | counter++ 68 | request({url:url,headers:{'accept':'text/html'}}, function (e, resp, body) { 69 | if (e) throw e 70 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 71 | assert.equal(resp.headers['content-type'], 'text/html') 72 | assert.equal(body, 'Hello World') 73 | console.log('Passed html /') 74 | end() 75 | }) 76 | 77 | counter++ 78 | request({url:url}, function (e, resp, body) { 79 | if (e) throw e 80 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 81 | assert.equal(resp.headers['content-type'], 'application/json') 82 | assert.equal(body, JSON.stringify({"text":"hello world"})) 83 | console.log('Passed no headers /') 84 | end() 85 | }) 86 | 87 | counter++ 88 | request({url:url+'static',headers:{'accept':'application/json'}}, function (e, resp, body) { 89 | if (e) throw e 90 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 91 | assert.equal(resp.headers['content-type'], 'application/json') 92 | assert.equal(body, JSON.stringify({text:'hello world'})) 93 | console.log('Passed json /static') 94 | end() 95 | }) 96 | 97 | counter++ 98 | request({url:url+'static'}, function (e, resp, body) { 99 | if (e) throw e 100 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 101 | assert.equal(resp.headers['content-type'], 'application/json') 102 | console.log('Passed 406 /static') 103 | end() 104 | }) 105 | 106 | counter++ 107 | request({url:url+'static',headers:{'accept':'text/html'}}, function (e, resp, body) { 108 | if (e) throw e 109 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 110 | assert.equal(resp.headers['content-type'], 'text/html') 111 | assert.equal(body, 'Hello World') 112 | console.log('Passed html /static') 113 | end() 114 | }) 115 | 116 | counter++ 117 | request({url:url+'buffer',headers:{'accept':'text/html'}}, function (e, resp, body) { 118 | if (e) throw e 119 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 120 | assert.equal(resp.headers['content-type'], 'text/html') 121 | assert.equal(body, 'Hello World') 122 | console.log('Passed html /buffer') 123 | end() 124 | }) 125 | 126 | counter++ 127 | request({url:url+404, headers:{'accept':'text/html'}}, function (e, resp, body) { 128 | if (e) throw e 129 | if (resp.statusCode !== 404) throw new Error('status code is not 404. '+resp.statusCode) 130 | assert.equal(resp.headers['content-type'], 'text/html') 131 | assert.equal(body, 'Not Found') 132 | console.log('Passed html /404') 133 | end() 134 | }) 135 | 136 | counter++ 137 | request({url:url+404, headers:{'accept':'application/json'}}, function (e, resp, body) { 138 | if (e) throw e 139 | if (resp.statusCode !== 404) throw new Error('status code is not 404. '+resp.statusCode) 140 | assert.equal(resp.headers['content-type'], 'application/json') 141 | assert.equal(body, "{\"status\":404,\"reason\":\"not found\",\"message\":\"not found\"}") 142 | console.log('Passed json /404') 143 | end() 144 | }) 145 | 146 | counter++ 147 | request({url:url+404}, function (e, resp, body) { 148 | if (e) throw e 149 | if (resp.statusCode !== 404) throw new Error('status code is not 404. '+resp.statusCode) 150 | assert.equal(resp.headers['content-type'], 'text/plain') 151 | assert.equal(body, "Not Found") 152 | console.log('Passed default text/plain /404') 153 | end() 154 | }) 155 | 156 | counter++ 157 | request.put( 158 | { url:url+'puts' 159 | , json: {code:200, test:'asdfasdf'} 160 | } 161 | , 162 | function (e, resp, body) { 163 | if (e) throw e 164 | if (resp.statusCode !== 201) throw new Error('status code is not 201. '+resp.statusCode) 165 | assert.equal(resp.headers['content-type'], 'application/json') 166 | assert.deepEqual(body, {code:200, test:'asdfasdf'}) 167 | console.log('Passed json /put') 168 | end() 169 | } 170 | ) 171 | 172 | counter++ 173 | request.get(url+'file/js', function (e, resp, body) { 174 | if (e) throw e 175 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 176 | assert.equal(resp.headers['content-type'], 'text/javascript') 177 | assert.equal(body, fs.readFileSync(__filename).toString()) 178 | console.log('Passed /file/js') 179 | end() 180 | }) 181 | 182 | counter++ 183 | request.get(url+'files/test-basic.js', function (e, resp, body) { 184 | if (e) throw e 185 | if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode) 186 | assert.equal(resp.headers['content-type'], 'text/javascript') 187 | assert.equal(body, fs.readFileSync(__filename).toString()) 188 | console.log('Passed /files/test-tako.js') 189 | end() 190 | }) 191 | 192 | }) 193 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | , events = require('events') 3 | , crypto = require('crypto') 4 | , path = require('path') 5 | , url = require('url') 6 | , fs = require('fs') 7 | , util = require('util') 8 | , stream = require('stream') 9 | , qs = require('querystring') 10 | , http = require('http') 11 | , https = require('https') 12 | // Dependencies 13 | , mapleTree = require('mapleTree') 14 | , filed = require('filed') 15 | // Local Imports 16 | , handlebars = require('./handlebars') 17 | , rfc822 = require('./rfc822') 18 | , io = null 19 | ; 20 | 21 | try { 22 | io = require('socket.io') 23 | } catch (er) { 24 | // oh well, no socket.io. 25 | } 26 | 27 | var cap = function (stream, limit) { 28 | if (!limit) limit = Infinity 29 | stream.caplimit = limit 30 | stream.bufferedData = [] 31 | stream.bufferedLength = 0 32 | 33 | stream._capemit = stream.emit 34 | stream.emit = function () { 35 | if (arguments[0] === 'data') { 36 | stream.bufferedData.push(arguments) 37 | stream.bufferedLength += arguments[1].length 38 | if (stream.bufferedLength > stream.caplimit) { 39 | stream.pause() 40 | } 41 | } else if (arguments[0] === 'end') { 42 | stream.ended = true 43 | } else { 44 | stream._capemit.apply(stream, arguments) 45 | } 46 | } 47 | 48 | stream.release = function () { 49 | stream.emit = stream._capemit 50 | while (stream.bufferedData.length) { 51 | stream.emit.apply(stream, stream.bufferedData.shift()) 52 | } 53 | if (stream.ended) stream.emit('end') 54 | if (stream.readable) stream.resume() 55 | } 56 | 57 | return stream 58 | } 59 | 60 | module.exports = function (options) { 61 | return new Application(options) 62 | } 63 | 64 | function BufferResponse (buffer, mimetype) { 65 | if (!Buffer.isBuffer(buffer)) this.body = new Buffer(buffer) 66 | else this.body = buffer 67 | this.timestamp = rfc822.getRFC822Date(new Date()) 68 | this.etag = crypto.createHash('md5').update(buffer).digest("hex") 69 | this.mimetype = mimetype 70 | this.cache = true 71 | } 72 | BufferResponse.prototype.request = function (req, resp) { 73 | if (resp._header) return // This response already started 74 | resp.setHeader('content-type', this.mimetype) 75 | if (this.cache) { 76 | resp.setHeader('last-modified', this.timestamp) 77 | resp.setHeader('etag', this.etag) 78 | } 79 | if (req.method !== 'GET' && req.method !== 'HEAD') { 80 | resp.statusCode = 405 81 | return (resp._end ? resp._end : resp.end).call(resp) 82 | } 83 | if (this.cache && 84 | req.headers['if-none-match'] === this.etag || 85 | req.headers['if-modified-since'] === this.timestamp 86 | ) { 87 | resp.statusCode = 304 88 | return (resp._end ? resp._end : resp.end).call(resp) 89 | } 90 | resp.statusCode = this.statusCode || 200 91 | ;(resp._write ? resp._write : resp.write).call(resp, this.body) 92 | return (resp._end ? resp._end : resp.end).call(resp) 93 | } 94 | 95 | function Page (templatename) { 96 | var self = this 97 | self.promises = {} 98 | self.counter = 0 99 | self.results = {} 100 | self.dests = [] 101 | self.on('pipe', function (src) { 102 | if (src.method && (src.method === 'PUT' || src.method == 'POST')) { 103 | var p = self.promise('body') 104 | src.on('error', function (e) { 105 | p(e) 106 | }) 107 | src.on('body', function (body) { 108 | p(null, body) 109 | }) 110 | if (src.json) { 111 | var jp = self.promise('json') 112 | src.on('json', function (obj) { 113 | jp(null, obj) 114 | }) 115 | } 116 | } 117 | }) 118 | process.nextTick(function () { 119 | if (self.listeners('error').length === 0) { 120 | self.on('error', function (err) { 121 | if (self.dests.length) { 122 | self.dests.forEach(function (resp) { 123 | if (resp.error) return resp.error(err) 124 | }) 125 | } else { 126 | self.application.logger.error('Page::Uncaught Error:') 127 | self.application.logger.error(err) 128 | } 129 | }) 130 | } 131 | 132 | if (templatename) { 133 | self.template(templatename) 134 | } 135 | if (self.counter === 0) self.emit('finish', self.results) 136 | }) 137 | } 138 | util.inherits(Page, stream.Stream) 139 | Page.prototype.promise = function (name, cb) { 140 | if (name === 'error') throw new Error("You cannot name a promise 'error'") 141 | if (name === 'finish') throw new Error("You cannot name a promise 'finish'") 142 | if (name === 'resolved') throw new Error("You cannot name a promise 'resolved'") 143 | var self = this; 144 | self.counter += 1 145 | self.promises[name] = function (e, result) { 146 | self.emit('resolved', name, e, result) 147 | if (e) { 148 | e.promise = name 149 | return self.emit('error', e, name) 150 | } 151 | self.emit(name, result) 152 | self.results[name] = result 153 | self.counter = self.counter - 1 154 | if (self.counter === 0) self.emit('finish', self.results) 155 | } 156 | if (cb) self.on(name, cb) 157 | return self.promises[name] 158 | } 159 | Page.prototype.event = function (name, cb) { 160 | var p = this.promise(name, cb) 161 | , r = function (r) { p(null, r) } 162 | ; 163 | return r 164 | } 165 | Page.prototype.pipe = function (dest) { 166 | this.dests.push(dest) 167 | } 168 | Page.prototype.write = function () {} 169 | Page.prototype.end = function () {} 170 | Page.prototype.destroy = function () {} 171 | 172 | // Templates implementation 173 | function Templates (app) { 174 | this.files = {} 175 | this.loaded = true 176 | this.after = {} 177 | this.app = app 178 | this.names = {} 179 | this.loading = 0 180 | this.tempcache = {} 181 | this.Template = function (text) { 182 | this.compiled = handlebars.compile(text) 183 | } 184 | this.Template.prototype.render = function (obj) { 185 | return new Buffer(this.compiled(obj)) 186 | } 187 | } 188 | util.inherits(Templates, events.EventEmitter) 189 | Templates.prototype.get = function (name, cb) { 190 | var self = this 191 | 192 | if (name.indexOf(' ') !== -1 || name[0] === '<') { 193 | process.nextTick(function () { 194 | if (!self.tempcache[name]) { 195 | self.tempcache[name] = new self.Template(name) 196 | } 197 | cb(null, self.tempcache[name]) 198 | }) 199 | return 200 | } 201 | 202 | function finish () { 203 | if (name in self.names) { 204 | cb(null, self.files[self.names[name]]) 205 | } else { 206 | cb(new Error("Cannot find template")) 207 | } 208 | } 209 | if (this.loaded) { 210 | process.nextTick(finish) 211 | } else { 212 | self.once('loaded', finish) 213 | } 214 | } 215 | Templates.prototype.directory = function (dir) { 216 | var self = this 217 | this.loaded = false 218 | this.loading += 1 219 | loadfiles(dir, function (e, filemap) { 220 | if (e) return self.emit('error', e) 221 | for (var i in filemap) { 222 | if (path.basename(i).charAt(0) === '.') continue 223 | self.files[i] = new self.Template(filemap[i]) 224 | self.names[path.basename(i)] = i 225 | self.names[path.basename(i, path.extname(i))] = i 226 | } 227 | self.loading -= 1 228 | self.loaded = true 229 | if (self.loading === 0) self.emit('loaded') 230 | }) 231 | } 232 | 233 | function loadfiles (f, cb) { 234 | var filesmap = {} 235 | fs.readdir(f, function (e, files) { 236 | if (e) return cb(e) 237 | var counter = 0 238 | files.forEach(function (filename) { 239 | counter += 1 240 | fs.stat(path.join(f, filename), function (e, stat) { 241 | if (stat.isDirectory()) { 242 | loadfiles(path.join(f, filename), function (e, files) { 243 | if (e) return cb(e) 244 | for (var i in files) { 245 | filesmap[i] = files[i] 246 | } 247 | counter -= 1 248 | if (counter === 0) cb(null, filesmap) 249 | }) 250 | } else { 251 | fs.readFile(path.join(f, filename), function (e, data) { 252 | filesmap[path.join(f, filename)] = data.toString() 253 | counter -= 1 254 | if (counter === 0) cb(null, filesmap) 255 | }) 256 | } 257 | }) 258 | }) 259 | }) 260 | } 261 | 262 | function Application (options) { 263 | var self = this 264 | if (!options) options = {} 265 | self.options = options 266 | self.addHeaders = {} 267 | if (self.options.logger) { 268 | self.logger = self.options.logger 269 | } 270 | 271 | self.onRequest = function (req, resp) { 272 | if (self.logger.info) self.logger.info('Request', req.url, req.headers) 273 | // Opt out entirely if this is a socketio request 274 | if (self.socketio && req.url.slice(0, '/socket.io/'.length) === '/socket.io/') { 275 | return self._ioEmitter.emit('request', req, resp) 276 | } 277 | 278 | for (var hdr in self.addHeaders) { 279 | resp.setHeader(hdr, self.addHeaders[hdr]) 280 | } 281 | 282 | req.accept = function () { 283 | if (!req.headers.accept) return '*/*' 284 | var cc = null 285 | var pos = 99999999 286 | for (var i=arguments.length-1;i!==-1;i--) { 287 | var ipos = req.headers.accept.indexOf(arguments[i]) 288 | if ( ipos !== -1 && ipos < pos ) cc = arguments[i] 289 | } 290 | return cc 291 | } 292 | 293 | resp.error = function (err) { 294 | if (typeof(err) === "string") err = {message: err} 295 | if (!err.statusCode) err.statusCode = 500 296 | resp.statusCode = err.statusCode || 500 297 | self.logger.log('error %statusCode "%message "', err) 298 | resp.end(err.message || err) // this should be better 299 | } 300 | 301 | resp.notfound = function (log) { 302 | if (log) self.logger.log(log) 303 | self.notfound(req, resp) 304 | } 305 | 306 | // Get all the parsed url properties on the request 307 | // This is the same style express uses and it's quite nice 308 | var parsed = url.parse(req.url) 309 | for (var i in parsed) { 310 | req[i] = parsed[i] 311 | } 312 | 313 | if (req.query) req.qs = qs.parse(req.query) 314 | 315 | req.route = self.router.match(req.pathname) 316 | 317 | if (!req.route || !req.route.perfect) return self.notfound(req, resp) 318 | 319 | req.params = req.route.params 320 | 321 | var onWrites = [] 322 | resp._write = resp.write 323 | resp.write = function () { 324 | if (resp.statusCode === 404 && self._notfound) { 325 | return self._notfound.request(req, resp) 326 | } 327 | if (onWrites.length === 0) return resp._write.apply(resp, arguments) 328 | var args = arguments 329 | onWrites.forEach(function (onWrite) { 330 | var c = onWrite.apply(resp, args) 331 | if (c !== undefined) args[0] = c 332 | }) 333 | return resp._write.apply(resp, args) 334 | } 335 | 336 | // Fix for node's premature header check in end() 337 | resp._end = resp.end 338 | resp.end = function (chunk) { 339 | if (resp.statusCode === 404 && self._notfound) { 340 | return self._notfound.request(req, resp) 341 | } 342 | if (chunk) resp.write(chunk) 343 | resp._end() 344 | self.logger.info('Response', resp.statusCode, req.url, resp._headers) 345 | } 346 | 347 | self.emit('request', req, resp) 348 | 349 | 350 | req.route.fn.call(req.route, req, resp, self.authHandler) 351 | 352 | if (req.listeners('body').length) { 353 | var buffer = '' 354 | req.on('data', function (chunk) { 355 | buffer += chunk 356 | }) 357 | req.on('end', function (chunk) { 358 | if (chunk) buffer += chunk 359 | req.emit('body', buffer) 360 | }) 361 | } 362 | } 363 | 364 | self.router = new mapleTree.RouteTree() 365 | self.on('newroute', function (route) { 366 | self.router.define(route.path, function (req, resp, authHandler){ 367 | route.handler(req, resp, authHandler) 368 | }) 369 | }) 370 | 371 | self.templates = new Templates(self) 372 | 373 | // Default to having json enabled 374 | self.on('request', JSONRequestHandler) 375 | 376 | // setup servers 377 | self.http = options.http || {} 378 | self.https = options.https || {} 379 | if (io) { 380 | self.socketio = options.socketio === undefined ? {} : options.socketio 381 | if (!self.socketio.logger && self.logger) { 382 | self.socketio.logger = self.logger 383 | } 384 | } else if (options.socketio) { 385 | throw new Error('socket.io is not available'); 386 | } 387 | 388 | self.httpServer = http.createServer() 389 | self.httpsServer = https.createServer(self.https) 390 | 391 | self.httpServer.on('request', self.onRequest) 392 | self.httpsServer.on('request', self.onRequest) 393 | 394 | var _listenProxied = false 395 | var listenProxy = function () { 396 | if (!_listenProxied && self._ioEmitter) self._ioEmitter.emit('listening') 397 | _listenProxied = true 398 | } 399 | 400 | self.httpServer.on('listening', listenProxy) 401 | self.httpsServer.on('listening', listenProxy) 402 | 403 | if (io && self.socketio) { 404 | // setup socket.io 405 | self._ioEmitter = new events.EventEmitter() 406 | 407 | self.httpServer.on('upgrade', function (request, socket, head) { 408 | self._ioEmitter.emit('upgrade', request, socket, head) 409 | }) 410 | self.httpsServer.on('upgrade', function (request, socket, head) { 411 | self._ioEmitter.emit('upgrade', request, socket, head) 412 | }) 413 | 414 | self.socketioManager = new io.Manager(self._ioEmitter, self.socketio) 415 | self.sockets = self.socketioManager.sockets 416 | } 417 | 418 | if (!self.logger) { 419 | self.logger = 420 | { log: console.log 421 | , error: console.error 422 | , info: function () {} 423 | } 424 | } 425 | } 426 | util.inherits(Application, events.EventEmitter) 427 | 428 | Application.prototype.addHeader = function (name, value) { 429 | this.addHeaders[name] = value 430 | } 431 | 432 | Application.prototype.route = function (path, cb) { 433 | var r = new Route(path, this) 434 | if (cb) r.on('request', cb) 435 | return r 436 | } 437 | Application.prototype.middle = function (mid) { 438 | throw new Error('Middleware is dumb. Just listen to the app "request" event.') 439 | } 440 | 441 | Application.prototype.listen = function (createServer, port, cb) { 442 | var self = this 443 | if (!cb) { 444 | cb = port 445 | port = createServer 446 | } 447 | self.server = createServer(function (req, resp) { 448 | self.onRequest(req, resp) 449 | }) 450 | self.server.listen(port, cb) 451 | return this 452 | } 453 | Application.prototype.close = function (cb) { 454 | var counter = 1 455 | , self = this 456 | ; 457 | function end () { 458 | counter = counter - 1 459 | self.emit('close') 460 | if (io && self.socketio) { 461 | self._ioEmitter.emit('close') 462 | } 463 | if (counter === 0 && cb) cb() 464 | } 465 | if (self.httpServer._handle) { 466 | counter++ 467 | self.httpServer.once('close', end) 468 | self.httpServer.close() 469 | } 470 | if (self.httpsServer._handle) { 471 | counter++ 472 | self.httpsServer.once('close', end) 473 | self.httpsServer.close() 474 | } 475 | end() 476 | return self 477 | } 478 | Application.prototype.notfound = function (req, resp) { 479 | if (!resp) { 480 | if (typeof req === "string") { 481 | if (req[0] === '/') req = new BufferResponse(fs.readFileSync(req), 'text/html') 482 | else req = new BufferResponse(req, 'text/html') 483 | } else if (typeof req === "function") { 484 | this._notfound = {} 485 | this._notfound.request = function (r, resp) { 486 | if (resp._write) resp.write = resp._write 487 | if (resp._end) resp.end = resp._end 488 | req(r, resp) 489 | } 490 | return 491 | } else if (typeof req === 'object') { 492 | req = new BufferResponse(JSON.stringify(req), 'application/json') 493 | } 494 | req.statusCode = 404 495 | req.cache = false 496 | this._notfound = req 497 | return 498 | } 499 | 500 | if (resp._header) return // This response already started 501 | 502 | if (this._notfound) return this._notfound.request(req, resp) 503 | 504 | var cc = req.accept('text/html', 'application/json', 'text/plain', '*/*') || 'text/plain' 505 | var body = 'Not Found' 506 | if (cc === '*/*') cc = 'text/plain' 507 | resp.statusCode = 404 508 | resp.setHeader('content-type', cc) 509 | if (cc === 'text/html') { 510 | body = 'Not Found' 511 | } else if (cc === 'application/json') { 512 | body = JSON.stringify({status:404, reason:'not found', message:'not found'}) 513 | } 514 | resp.end(body) 515 | } 516 | Application.prototype.auth = function (handler) { 517 | if (!handler) return this.authHandler 518 | this.authHandler = handler 519 | } 520 | Application.prototype.page = function () { 521 | var page = new Page() 522 | , self = this 523 | ; 524 | page.application = self 525 | page.template = function (name) { 526 | var p = page.promise("template") 527 | self.templates.get(name, function (e, template) { 528 | if (e) return p(e) 529 | if (p.src) p.src.pipe(template) 530 | page.on('finish', function () { 531 | process.nextTick(function () { 532 | var text = template.render(page.results) 533 | page.dests.forEach(function (d) { 534 | if (d._header) return // Don't try to write to a response that's already finished 535 | if (d.writeHead) { 536 | d.statusCode = 200 537 | d.setHeader('content-type', page.mimetype || 'text/html') 538 | d.setHeader('content-length', text.length) 539 | } 540 | d.write(text) 541 | d.end() 542 | }) 543 | }) 544 | }) 545 | p(null, template) 546 | }) 547 | } 548 | return page 549 | } 550 | 551 | module.exports.JSONRequestHandler = JSONRequestHandler 552 | function JSONRequestHandler (req, resp) { 553 | var orig = resp.write 554 | resp.write = function (chunk) { 555 | if (resp._header) return orig.call(this, chunk) // This response already started 556 | // bail fast for chunks to limit impact on streaming 557 | if (Buffer.isBuffer(chunk)) return orig.call(this, chunk) 558 | // if it's an object serialize it and set proper headers 559 | if (typeof chunk === 'object') { 560 | chunk = new Buffer(JSON.stringify(chunk)) 561 | resp.setHeader('content-type', 'application/json') 562 | resp.setHeader('content-length', chunk.length) 563 | if (!resp.statusCode && (req.method === 'GET' || req.method === 'HEAD')) { 564 | resp.statusCode = 200 565 | } 566 | } 567 | return orig.call(resp, chunk) 568 | } 569 | if (req.method === "PUT" || req.method === "POST") { 570 | if (req.headers['content-type'] && (req.headers['content-type'].split(';')[0] === 'application/json')) { 571 | req.on('body', function (body) { 572 | try { 573 | req.emit('json', JSON.parse(body)); 574 | } catch (e) { 575 | req.emit('error', e); 576 | } 577 | }) 578 | } 579 | } 580 | } 581 | 582 | 583 | function Route (path, application) { 584 | // This code got really crazy really fast. 585 | // There are a lot of different states that close out of other logic. 586 | // This could be refactored but it's hard because there is so much 587 | // cascading logic. 588 | var self = this 589 | self.path = path 590 | self.app = application 591 | self.byContentType = {} 592 | 593 | var returnEarly = function (req, resp, keys, authHandler) { 594 | if (self._events && self._events['request']) { 595 | if (authHandler) { 596 | cap(req) 597 | authHandler(req, resp, function (user) { 598 | if (resp._header) return // This response already started 599 | req.user = user 600 | if (self._must && self._must.indexOf('auth') !== -1 && !req.user) { 601 | resp.statusCode = 403 602 | resp.setHeader('content-type', 'application/json') 603 | resp.end(JSON.stringify({error: 'This resource requires auth.'})) 604 | return 605 | } 606 | self.emit('request', req, resp) 607 | req.release() 608 | }) 609 | } else { 610 | if (resp._header) return // This response already started 611 | if (self._must && self._must.indexOf('auth') !== -1 && !req.user) { 612 | resp.statusCode = 403 613 | resp.setHeader('content-type', 'application/json') 614 | resp.end(JSON.stringify({error: 'This resource requires auth.'})) 615 | return 616 | } 617 | self.emit('request', req, resp) 618 | } 619 | } else { 620 | if (resp._header) return // This response already started 621 | resp.statusCode = 406 622 | resp.setHeader('content-type', 'text/plain') 623 | resp.end('Request does not include a valid mime-type for this resource: '+keys.join(', ')) 624 | } 625 | } 626 | 627 | self.handler = function (req, resp, authHandler) { 628 | if (self._methods && self._methods.indexOf(req.method) === -1) { 629 | resp.statusCode = 405 630 | resp.end('Method not Allowed.') 631 | return 632 | } 633 | 634 | self.emit('before', req, resp) 635 | if (self.authHandler) { 636 | authHandler = self.authHandler 637 | } 638 | 639 | var keys = Object.keys(self.byContentType).concat(['*/*']) 640 | if (keys.length) { 641 | if (req.method !== 'PUT' && req.method !== 'POST') { 642 | var cc = req.accept.apply(req, keys) 643 | } else { 644 | if(req.headers['content-type']) 645 | var cc = req.headers['content-type'].split(';')[0]; 646 | else 647 | var cc = false; 648 | } 649 | 650 | if (!cc) return returnEarly(req, resp, keys, authHandler) 651 | if (cc === '*/*') { 652 | var h = this.byContentType[Object.keys(this.byContentType)[0]] 653 | } else { 654 | var h = this.byContentType[cc] 655 | } 656 | if (!h) return returnEarly(req, resp, keys, authHandler) 657 | if (resp._header) return // This response already started 658 | resp.setHeader('content-type', cc) 659 | 660 | var run = function () { 661 | if (h.request) { 662 | return h.request(req, resp) 663 | } 664 | if (h.pipe) { 665 | req.pipe(h) 666 | h.pipe(resp) 667 | return 668 | } 669 | h.call(req.route, req, resp) 670 | } 671 | 672 | if (authHandler) { 673 | cap(req) 674 | authHandler(req, resp, function (user) { 675 | req.user = user 676 | if (self._must && self._must.indexOf('auth') !== -1 && !req.user) { 677 | if (resp._header) return // This response already started 678 | resp.statusCode = 403 679 | resp.setHeader('content-type', 'application/json') 680 | resp.end(JSON.stringify({error: 'This resource requires auth.'})) 681 | return 682 | } 683 | run() 684 | req.release() 685 | }) 686 | } else { 687 | if (resp._header) return // This response already started 688 | if (self._must && self._must.indexOf('auth') !== -1) { 689 | resp.statusCode = 403 690 | resp.setHeader('content-type', 'application/json') 691 | resp.end(JSON.stringify({error: 'This resource requires auth.'})) 692 | return 693 | } 694 | run() 695 | } 696 | 697 | } else { 698 | returnEarly(req, resp, keys, authHandler) 699 | } 700 | } 701 | application.emit('newroute', self) 702 | } 703 | util.inherits(Route, events.EventEmitter) 704 | Route.prototype.json = function (cb) { 705 | if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'application/json') 706 | else if (typeof cb === 'object') cb = new BufferResponse(JSON.stringify(cb), 'application/json') 707 | else if (typeof cb === 'string') { 708 | if (cb[0] === '/') cb = filed(cb) 709 | else cb = new BufferResponse(cb, 'application/json') 710 | } 711 | this.byContentType['application/json'] = cb 712 | return this 713 | } 714 | Route.prototype.html = function (cb) { 715 | if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'text/html') 716 | else if (typeof cb === 'string') { 717 | if (cb[0] === '/') cb = filed(cb) 718 | else cb = new BufferResponse(cb, 'text/html') 719 | } 720 | this.byContentType['text/html'] = cb 721 | return this 722 | } 723 | Route.prototype.text = function (cb) { 724 | if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'text/plain') 725 | else if (typeof cb === 'string') { 726 | if (cb[0] === '/') cb = filed(cb) 727 | else cb = new BufferResponse(cb, 'text/plain') 728 | } 729 | this.byContentType['text/plain'] = cb 730 | return this 731 | } 732 | 733 | Route.prototype.file = function (filepath) { 734 | this.on('request', function (req, resp) { 735 | var f = filed(filepath) 736 | req.pipe(f) 737 | f.pipe(resp) 738 | }) 739 | return this 740 | } 741 | Route.prototype.files = function (filepath) { 742 | this.on('request', function (req, resp) { 743 | req.route.extras.unshift(filepath) 744 | var p = path.join.apply(path.join, req.route.extras) 745 | if (p.slice(0, filepath.length) !== filepath) { 746 | resp.statusCode = 403 747 | return resp.end('Naughty Naughty!') 748 | } 749 | var f = filed(p) 750 | req.pipe(f) 751 | f.pipe(resp) 752 | }) 753 | return this 754 | } 755 | Route.prototype.auth = function (handler) { 756 | if (!handler) return this.authHandler 757 | this.authHandler = handler 758 | return this 759 | } 760 | Route.prototype.must = function () { 761 | this._must = Array.prototype.slice.call(arguments) 762 | return this 763 | } 764 | Route.prototype.methods = function () { 765 | this._methods = Array.prototype.slice.call(arguments) 766 | return this 767 | } 768 | 769 | function ServiceError(msg) { 770 | Error.apply(this, arguments) 771 | this.message = msg 772 | this.stack = (new Error()).stack; 773 | } 774 | ServiceError.prototype = new Error() 775 | ServiceError.prototype.constructor = ServiceError 776 | ServiceError.prototype.name = 'ServiceError' 777 | module.exports.ServiceError = ServiceError 778 | 779 | function Router (hosts, options) { 780 | var self = this 781 | self.hosts = hosts || {} 782 | self.options = options || {} 783 | 784 | function makeHandler (type) { 785 | var handler = function (req, resp) { 786 | var host = req.headers.host 787 | if (!host || !self.hosts[host]) { 788 | if (!self._default) { 789 | resp.writeHead(404, {'content-type':'text/plain'}) 790 | resp.end('No host header.') 791 | } else { 792 | self._default.httpServer.emit(type, req, resp) 793 | } 794 | return 795 | } 796 | self.hosts[host].httpServer.emit(type, req, resp) 797 | } 798 | return handler 799 | } 800 | 801 | self.httpServer = http.createServer() 802 | self.httpsServer = https.createServer(self.options.ssl || {}) 803 | 804 | self.httpServer.on('request', makeHandler('request')) 805 | self.httpsServer.on('request', makeHandler('request')) 806 | 807 | self.httpServer.on('upgrade', makeHandler('upgrade')) 808 | self.httpsServer.on('upgrade', makeHandler('upgrade')) 809 | } 810 | Router.prototype.host = function (host, app) { 811 | this.hosts[host] = app 812 | } 813 | Router.prototype.default = function (app) { 814 | this._default = app 815 | } 816 | Router.prototype.close = function (cb) { 817 | var counter = 1 818 | , self = this 819 | ; 820 | function end () { 821 | counter = counter - 1 822 | if (counter === 0 && cb) cb() 823 | } 824 | if (self.httpServer._handle) { 825 | counter++ 826 | self.httpServer.once('close', end) 827 | self.httpServer.close() 828 | } 829 | if (self.httpsServer._handle) { 830 | counter++ 831 | self.httpsServer.once('close', end) 832 | self.httpsServer.close() 833 | } 834 | 835 | for (var i in self.hosts) { 836 | counter++ 837 | process.nextTick(function () { 838 | self.hosts[i].close(end) 839 | }) 840 | 841 | } 842 | end() 843 | } 844 | 845 | module.exports.router = function (hosts) {return new Router(hosts)} 846 | 847 | -------------------------------------------------------------------------------- /handlebars.js: -------------------------------------------------------------------------------- 1 | // lib/handlebars/parser.js 2 | /* Jison generated parser */ 3 | var handlebars = (function(){ 4 | var parser = {trace: function trace() { }, 5 | yy: {}, 6 | symbols_: {"error":2,"root":3,"program":4,"EOF":5,"statements":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"params":25,"hash":26,"param":27,"STRING":28,"INTEGER":29,"BOOLEAN":30,"hashSegments":31,"hashSegment":32,"ID":33,"EQUALS":34,"pathSegments":35,"SEP":36,"$accept":0,"$end":1}, 7 | terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",28:"STRING",29:"INTEGER",30:"BOOLEAN",33:"ID",34:"EQUALS",36:"SEP"}, 8 | productions_: [0,[3,2],[4,3],[4,1],[4,0],[6,1],[6,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[7,2],[17,3],[17,2],[17,2],[17,1],[25,2],[25,1],[27,1],[27,1],[27,1],[27,1],[26,1],[31,2],[31,1],[32,3],[32,3],[32,3],[32,3],[21,1],[35,3],[35,1]], 9 | performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { 10 | 11 | var $0 = $$.length - 1; 12 | switch (yystate) { 13 | case 1: return $$[$0-1] 14 | break; 15 | case 2: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]) 16 | break; 17 | case 3: this.$ = new yy.ProgramNode($$[$0]) 18 | break; 19 | case 4: this.$ = new yy.ProgramNode([]) 20 | break; 21 | case 5: this.$ = [$$[$0]] 22 | break; 23 | case 6: $$[$0-1].push($$[$0]); this.$ = $$[$0-1] 24 | break; 25 | case 7: this.$ = new yy.InverseNode($$[$0-2], $$[$0-1], $$[$0]) 26 | break; 27 | case 8: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0]) 28 | break; 29 | case 9: this.$ = $$[$0] 30 | break; 31 | case 10: this.$ = $$[$0] 32 | break; 33 | case 11: this.$ = new yy.ContentNode($$[$0]) 34 | break; 35 | case 12: this.$ = new yy.CommentNode($$[$0]) 36 | break; 37 | case 13: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]) 38 | break; 39 | case 14: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]) 40 | break; 41 | case 15: this.$ = $$[$0-1] 42 | break; 43 | case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]) 44 | break; 45 | case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true) 46 | break; 47 | case 18: this.$ = new yy.PartialNode($$[$0-1]) 48 | break; 49 | case 19: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]) 50 | break; 51 | case 20: 52 | break; 53 | case 21: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]] 54 | break; 55 | case 22: this.$ = [[$$[$0-1]].concat($$[$0]), null] 56 | break; 57 | case 23: this.$ = [[$$[$0-1]], $$[$0]] 58 | break; 59 | case 24: this.$ = [[$$[$0]], null] 60 | break; 61 | case 25: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 62 | break; 63 | case 26: this.$ = [$$[$0]] 64 | break; 65 | case 27: this.$ = $$[$0] 66 | break; 67 | case 28: this.$ = new yy.StringNode($$[$0]) 68 | break; 69 | case 29: this.$ = new yy.IntegerNode($$[$0]) 70 | break; 71 | case 30: this.$ = new yy.BooleanNode($$[$0]) 72 | break; 73 | case 31: this.$ = new yy.HashNode($$[$0]) 74 | break; 75 | case 32: $$[$0-1].push($$[$0]); this.$ = $$[$0-1] 76 | break; 77 | case 33: this.$ = [$$[$0]] 78 | break; 79 | case 34: this.$ = [$$[$0-2], $$[$0]] 80 | break; 81 | case 35: this.$ = [$$[$0-2], new yy.StringNode($$[$0])] 82 | break; 83 | case 36: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])] 84 | break; 85 | case 37: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])] 86 | break; 87 | case 38: this.$ = new yy.IdNode($$[$0]) 88 | break; 89 | case 39: $$[$0-2].push($$[$0]); this.$ = $$[$0-2]; 90 | break; 91 | case 40: this.$ = [$$[$0]] 92 | break; 93 | } 94 | }, 95 | table: [{3:1,4:2,5:[2,4],6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{1:[3]},{5:[1,16]},{5:[2,3],7:17,8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,19],20:[2,3],22:[1,13],23:[1,14],24:[1,15]},{5:[2,5],14:[2,5],15:[2,5],16:[2,5],19:[2,5],20:[2,5],22:[2,5],23:[2,5],24:[2,5]},{4:20,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{4:21,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{17:22,21:23,33:[1,25],35:24},{17:26,21:23,33:[1,25],35:24},{17:27,21:23,33:[1,25],35:24},{17:28,21:23,33:[1,25],35:24},{21:29,33:[1,25],35:24},{1:[2,1]},{6:30,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{5:[2,6],14:[2,6],15:[2,6],16:[2,6],19:[2,6],20:[2,6],22:[2,6],23:[2,6],24:[2,6]},{17:22,18:[1,31],21:23,33:[1,25],35:24},{10:32,20:[1,33]},{10:34,20:[1,33]},{18:[1,35]},{18:[2,24],21:40,25:36,26:37,27:38,28:[1,41],29:[1,42],30:[1,43],31:39,32:44,33:[1,45],35:24},{18:[2,38],28:[2,38],29:[2,38],30:[2,38],33:[2,38],36:[1,46]},{18:[2,40],28:[2,40],29:[2,40],30:[2,40],33:[2,40],36:[2,40]},{18:[1,47]},{18:[1,48]},{18:[1,49]},{18:[1,50],21:51,33:[1,25],35:24},{5:[2,2],8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,2],22:[1,13],23:[1,14],24:[1,15]},{14:[2,20],15:[2,20],16:[2,20],19:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,7],14:[2,7],15:[2,7],16:[2,7],19:[2,7],20:[2,7],22:[2,7],23:[2,7],24:[2,7]},{21:52,33:[1,25],35:24},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{18:[2,22],21:40,26:53,27:54,28:[1,41],29:[1,42],30:[1,43],31:39,32:44,33:[1,45],35:24},{18:[2,23]},{18:[2,26],28:[2,26],29:[2,26],30:[2,26],33:[2,26]},{18:[2,31],32:55,33:[1,56]},{18:[2,27],28:[2,27],29:[2,27],30:[2,27],33:[2,27]},{18:[2,28],28:[2,28],29:[2,28],30:[2,28],33:[2,28]},{18:[2,29],28:[2,29],29:[2,29],30:[2,29],33:[2,29]},{18:[2,30],28:[2,30],29:[2,30],30:[2,30],33:[2,30]},{18:[2,33],33:[2,33]},{18:[2,40],28:[2,40],29:[2,40],30:[2,40],33:[2,40],34:[1,57],36:[2,40]},{33:[1,58]},{14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,17],14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]},{18:[1,59]},{18:[1,60]},{18:[2,21]},{18:[2,25],28:[2,25],29:[2,25],30:[2,25],33:[2,25]},{18:[2,32],33:[2,32]},{34:[1,57]},{21:61,28:[1,62],29:[1,63],30:[1,64],33:[1,25],35:24},{18:[2,39],28:[2,39],29:[2,39],30:[2,39],33:[2,39],36:[2,39]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{18:[2,34],33:[2,34]},{18:[2,35],33:[2,35]},{18:[2,36],33:[2,36]},{18:[2,37],33:[2,37]}], 96 | defaultActions: {16:[2,1],37:[2,23],53:[2,21]}, 97 | parseError: function parseError(str, hash) { 98 | throw new Error(str); 99 | }, 100 | parse: function parse(input) { 101 | var self = this, 102 | stack = [0], 103 | vstack = [null], // semantic value stack 104 | lstack = [], // location stack 105 | table = this.table, 106 | yytext = '', 107 | yylineno = 0, 108 | yyleng = 0, 109 | recovering = 0, 110 | TERROR = 2, 111 | EOF = 1; 112 | 113 | //this.reductionCount = this.shiftCount = 0; 114 | 115 | this.lexer.setInput(input); 116 | this.lexer.yy = this.yy; 117 | this.yy.lexer = this.lexer; 118 | if (typeof this.lexer.yylloc == 'undefined') 119 | this.lexer.yylloc = {}; 120 | var yyloc = this.lexer.yylloc; 121 | lstack.push(yyloc); 122 | 123 | if (typeof this.yy.parseError === 'function') 124 | this.parseError = this.yy.parseError; 125 | 126 | function popStack (n) { 127 | stack.length = stack.length - 2*n; 128 | vstack.length = vstack.length - n; 129 | lstack.length = lstack.length - n; 130 | } 131 | 132 | function lex() { 133 | var token; 134 | token = self.lexer.lex() || 1; // $end = 1 135 | // if token isn't its numeric value, convert 136 | if (typeof token !== 'number') { 137 | token = self.symbols_[token] || token; 138 | } 139 | return token; 140 | }; 141 | 142 | var symbol, preErrorSymbol, state, action, a, r, yyval={},p,len,newState, expected; 143 | while (true) { 144 | // retreive state number from top of stack 145 | state = stack[stack.length-1]; 146 | 147 | // use default actions if available 148 | if (this.defaultActions[state]) { 149 | action = this.defaultActions[state]; 150 | } else { 151 | if (symbol == null) 152 | symbol = lex(); 153 | // read action for current state and first input 154 | action = table[state] && table[state][symbol]; 155 | } 156 | 157 | // handle parse error 158 | if (typeof action === 'undefined' || !action.length || !action[0]) { 159 | 160 | if (!recovering) { 161 | // Report error 162 | expected = []; 163 | for (p in table[state]) if (this.terminals_[p] && p > 2) { 164 | expected.push("'"+this.terminals_[p]+"'"); 165 | } 166 | var errStr = ''; 167 | if (this.lexer.showPosition) { 168 | errStr = 'Parse error on line '+(yylineno+1)+":\n"+this.lexer.showPosition()+'\nExpecting '+expected.join(', '); 169 | } else { 170 | errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " + 171 | (symbol == 1 /*EOF*/ ? "end of input" : 172 | ("'"+(this.terminals_[symbol] || symbol)+"'")); 173 | } 174 | this.parseError(errStr, 175 | {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 176 | } 177 | 178 | // just recovered from another error 179 | if (recovering == 3) { 180 | if (symbol == EOF) { 181 | throw new Error(errStr || 'Parsing halted.'); 182 | } 183 | 184 | // discard current lookahead and grab another 185 | yyleng = this.lexer.yyleng; 186 | yytext = this.lexer.yytext; 187 | yylineno = this.lexer.yylineno; 188 | yyloc = this.lexer.yylloc; 189 | symbol = lex(); 190 | } 191 | 192 | // try to recover from error 193 | while (1) { 194 | // check for error recovery rule in this state 195 | if ((TERROR.toString()) in table[state]) { 196 | break; 197 | } 198 | if (state == 0) { 199 | throw new Error(errStr || 'Parsing halted.'); 200 | } 201 | popStack(1); 202 | state = stack[stack.length-1]; 203 | } 204 | 205 | preErrorSymbol = symbol; // save the lookahead token 206 | symbol = TERROR; // insert generic error symbol as new lookahead 207 | state = stack[stack.length-1]; 208 | action = table[state] && table[state][TERROR]; 209 | recovering = 3; // allow 3 real symbols to be shifted before reporting a new error 210 | } 211 | 212 | // this shouldn't happen, unless resolve defaults are off 213 | if (action[0] instanceof Array && action.length > 1) { 214 | throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol); 215 | } 216 | 217 | switch (action[0]) { 218 | 219 | case 1: // shift 220 | //this.shiftCount++; 221 | 222 | stack.push(symbol); 223 | vstack.push(this.lexer.yytext); 224 | lstack.push(this.lexer.yylloc); 225 | stack.push(action[1]); // push state 226 | symbol = null; 227 | if (!preErrorSymbol) { // normal execution/no error 228 | yyleng = this.lexer.yyleng; 229 | yytext = this.lexer.yytext; 230 | yylineno = this.lexer.yylineno; 231 | yyloc = this.lexer.yylloc; 232 | if (recovering > 0) 233 | recovering--; 234 | } else { // error just occurred, resume old lookahead f/ before error 235 | symbol = preErrorSymbol; 236 | preErrorSymbol = null; 237 | } 238 | break; 239 | 240 | case 2: // reduce 241 | //this.reductionCount++; 242 | 243 | len = this.productions_[action[1]][1]; 244 | 245 | // perform semantic action 246 | yyval.$ = vstack[vstack.length-len]; // default to $$ = $1 247 | // default location, uses first token for firsts, last for lasts 248 | yyval._$ = { 249 | first_line: lstack[lstack.length-(len||1)].first_line, 250 | last_line: lstack[lstack.length-1].last_line, 251 | first_column: lstack[lstack.length-(len||1)].first_column, 252 | last_column: lstack[lstack.length-1].last_column 253 | }; 254 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 255 | 256 | if (typeof r !== 'undefined') { 257 | return r; 258 | } 259 | 260 | // pop off stack 261 | if (len) { 262 | stack = stack.slice(0,-1*len*2); 263 | vstack = vstack.slice(0, -1*len); 264 | lstack = lstack.slice(0, -1*len); 265 | } 266 | 267 | stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce) 268 | vstack.push(yyval.$); 269 | lstack.push(yyval._$); 270 | // goto new state = table[STATE][NONTERMINAL] 271 | newState = table[stack[stack.length-2]][stack[stack.length-1]]; 272 | stack.push(newState); 273 | break; 274 | 275 | case 3: // accept 276 | return true; 277 | } 278 | 279 | } 280 | 281 | return true; 282 | }};/* Jison generated lexer */ 283 | var lexer = (function(){var lexer = ({EOF:1, 284 | parseError:function parseError(str, hash) { 285 | if (this.yy.parseError) { 286 | this.yy.parseError(str, hash); 287 | } else { 288 | throw new Error(str); 289 | } 290 | }, 291 | setInput:function (input) { 292 | this._input = input; 293 | this._more = this._less = this.done = false; 294 | this.yylineno = this.yyleng = 0; 295 | this.yytext = this.matched = this.match = ''; 296 | this.conditionStack = ['INITIAL']; 297 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 298 | return this; 299 | }, 300 | input:function () { 301 | var ch = this._input[0]; 302 | this.yytext+=ch; 303 | this.yyleng++; 304 | this.match+=ch; 305 | this.matched+=ch; 306 | var lines = ch.match(/\n/); 307 | if (lines) this.yylineno++; 308 | this._input = this._input.slice(1); 309 | return ch; 310 | }, 311 | unput:function (ch) { 312 | this._input = ch + this._input; 313 | return this; 314 | }, 315 | more:function () { 316 | this._more = true; 317 | return this; 318 | }, 319 | pastInput:function () { 320 | var past = this.matched.substr(0, this.matched.length - this.match.length); 321 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 322 | }, 323 | upcomingInput:function () { 324 | var next = this.match; 325 | if (next.length < 20) { 326 | next += this._input.substr(0, 20-next.length); 327 | } 328 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 329 | }, 330 | showPosition:function () { 331 | var pre = this.pastInput(); 332 | var c = new Array(pre.length + 1).join("-"); 333 | return pre + this.upcomingInput() + "\n" + c+"^"; 334 | }, 335 | next:function () { 336 | if (this.done) { 337 | return this.EOF; 338 | } 339 | if (!this._input) this.done = true; 340 | 341 | var token, 342 | match, 343 | col, 344 | lines; 345 | if (!this._more) { 346 | this.yytext = ''; 347 | this.match = ''; 348 | } 349 | var rules = this._currentRules(); 350 | for (var i=0;i < rules.length; i++) { 351 | match = this._input.match(this.rules[rules[i]]); 352 | if (match) { 353 | lines = match[0].match(/\n.*/g); 354 | if (lines) this.yylineno += lines.length; 355 | this.yylloc = {first_line: this.yylloc.last_line, 356 | last_line: this.yylineno+1, 357 | first_column: this.yylloc.last_column, 358 | last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length} 359 | this.yytext += match[0]; 360 | this.match += match[0]; 361 | this.matches = match; 362 | this.yyleng = this.yytext.length; 363 | this._more = false; 364 | this._input = this._input.slice(match[0].length); 365 | this.matched += match[0]; 366 | token = this.performAction.call(this, this.yy, this, rules[i],this.conditionStack[this.conditionStack.length-1]); 367 | if (token) return token; 368 | else return; 369 | } 370 | } 371 | if (this._input === "") { 372 | return this.EOF; 373 | } else { 374 | this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 375 | {text: "", token: null, line: this.yylineno}); 376 | } 377 | }, 378 | lex:function lex() { 379 | var r = this.next(); 380 | if (typeof r !== 'undefined') { 381 | return r; 382 | } else { 383 | return this.lex(); 384 | } 385 | }, 386 | begin:function begin(condition) { 387 | this.conditionStack.push(condition); 388 | }, 389 | popState:function popState() { 390 | return this.conditionStack.pop(); 391 | }, 392 | _currentRules:function _currentRules() { 393 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 394 | }}); 395 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 396 | 397 | var YYSTATE=YY_START 398 | switch($avoiding_name_collisions) { 399 | case 0: this.begin("mu"); if (yy_.yytext) return 14; 400 | break; 401 | case 1: return 14; 402 | break; 403 | case 2: return 24; 404 | break; 405 | case 3: return 16; 406 | break; 407 | case 4: return 20; 408 | break; 409 | case 5: return 19; 410 | break; 411 | case 6: return 19; 412 | break; 413 | case 7: return 23; 414 | break; 415 | case 8: return 23; 416 | break; 417 | case 9: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.begin("INITIAL"); return 15; 418 | break; 419 | case 10: return 22; 420 | break; 421 | case 11: return 34; 422 | break; 423 | case 12: return 33; 424 | break; 425 | case 13: return 33; 426 | break; 427 | case 14: return 36; 428 | break; 429 | case 15: /*ignore whitespace*/ 430 | break; 431 | case 16: this.begin("INITIAL"); return 18; 432 | break; 433 | case 17: this.begin("INITIAL"); return 18; 434 | break; 435 | case 18: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 28; 436 | break; 437 | case 19: return 30; 438 | break; 439 | case 20: return 30; 440 | break; 441 | case 21: return 29; 442 | break; 443 | case 22: return 33; 444 | break; 445 | case 23: return 'INVALID'; 446 | break; 447 | case 24: return 5; 448 | break; 449 | } 450 | }; 451 | lexer.rules = [/^[^\x00]*?(?=(\{\{))/,/^[^\x00]+/,/^\{\{>/,/^\{\{#/,/^\{\{\//,/^\{\{\^/,/^\{\{\s*else\b/,/^\{\{\{/,/^\{\{&/,/^\{\{![\s\S]*?\}\}/,/^\{\{/,/^=/,/^\.(?=[} ])/,/^\.\./,/^[/.]/,/^\s+/,/^\}\}\}/,/^\}\}/,/^"(\\["]|[^"])*"/,/^true(?=[}\s])/,/^false(?=[}\s])/,/^[0-9]+(?=[}\s])/,/^[a-zA-Z0-9_$-]+(?=[=}\s/.])/,/^./,/^$/]; 452 | lexer.conditions = {"mu":{"rules":[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24],"inclusive":false},"INITIAL":{"rules":[0,1,24],"inclusive":true}};return lexer;})() 453 | parser.lexer = lexer; 454 | return parser; 455 | })(); 456 | 457 | // lib/handlebars/base.js 458 | var Handlebars = {}; 459 | 460 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 461 | module.exports = Handlebars; 462 | } 463 | 464 | Handlebars.VERSION = "1.0.beta.2"; 465 | 466 | Handlebars.Parser = handlebars; 467 | 468 | Handlebars.parse = function(string) { 469 | Handlebars.Parser.yy = Handlebars.AST; 470 | return Handlebars.Parser.parse(string); 471 | }; 472 | 473 | Handlebars.print = function(ast) { 474 | return new Handlebars.PrintVisitor().accept(ast); 475 | }; 476 | 477 | Handlebars.helpers = {}; 478 | Handlebars.partials = {}; 479 | 480 | Handlebars.registerHelper = function(name, fn, inverse) { 481 | if(inverse) { fn.not = inverse; } 482 | this.helpers[name] = fn; 483 | }; 484 | 485 | Handlebars.registerPartial = function(name, str) { 486 | this.partials[name] = str; 487 | }; 488 | 489 | Handlebars.registerHelper('helperMissing', function(arg) { 490 | if(arguments.length === 2) { 491 | return undefined; 492 | } else { 493 | throw new Error("Could not find property '" + arg + "'"); 494 | } 495 | }); 496 | 497 | Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) { 498 | inverse = inverse || function() {}; 499 | 500 | var ret = ""; 501 | var type = Object.prototype.toString.call(context); 502 | 503 | if(type === "[object Function]") { 504 | context = context(); 505 | } 506 | 507 | if(context === true) { 508 | return fn(this); 509 | } else if(context === false || context == null) { 510 | return inverse(this); 511 | } else if(type === "[object Array]") { 512 | if(context.length > 0) { 513 | for(var i=0, j=context.length; i