├── 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 Rogers " 6 | , "repository" : 7 | { "type" : "git" 8 | , "url" : "http://github.com/mikeal/tako.git" 9 | } 10 | , "bugs" : 11 | { "url" : "http://github.com/mikeal/tako/issues" } 12 | , "main" : "./index.html" 13 | , "dependencies": 14 | { "filed":">= 0.0.6" 15 | , "mapleTree":"*" 16 | } 17 | , "devDependencies": { "request": "2.9.x" } 18 | , "scripts": { "test": "node tests/run.js" } 19 | } 20 | -------------------------------------------------------------------------------- /tests/test-route.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , request = require('request') 3 | , common = require('./common') 4 | , tako = require('../') 5 | , app = tako() 6 | 7 | var URL = common.URL 8 | 9 | app.route('/hello', function (req, res) { 10 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 11 | res.end('/hello') 12 | }) 13 | 14 | app.route('/hello/:id', function (req, res) { 15 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 16 | res.end('/hello/' + req.params.id) 17 | }) 18 | 19 | app.httpServer.listen(common.PORT, function () { 20 | common.all( 21 | [ 22 | [ function (cb) { 23 | request(URL + '/hello', cb) 24 | } 25 | , function (err, res, body) { 26 | common.assertStatus(res, 200) 27 | assert.equal(body, '/hello') 28 | } 29 | ] 30 | , [ function (cb) { 31 | request(URL + '/nothing/here/dawg', cb) 32 | } 33 | , function (err, res, body) { 34 | common.assertStatus(res, 404) 35 | } 36 | ] 37 | , [ function (cb) { 38 | request(URL + '/hello/world', cb) 39 | } 40 | , function (err, res, body) { 41 | common.assertStatus(res, 200) 42 | assert.equal(body, '/hello/world') 43 | } 44 | ] 45 | ] 46 | , function () {app.close()}) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/test-hostrouter.js: -------------------------------------------------------------------------------- 1 | var tako = require('../index') 2 | , request = require('request') 3 | , assert = require('assert') 4 | , app1 = tako() 5 | , app2 = tako() 6 | , app3 = tako() 7 | , router = tako.router() 8 | , counter = 0 9 | ; 10 | 11 | app1.route('/name').text('app1') 12 | app2.route('/name').text('app2') 13 | app3.route('/name').text('app3') 14 | 15 | router.host('app1.localhost', app1) 16 | router.host('app2.localhost', app2) 17 | router.default(app3) 18 | 19 | 20 | 21 | function end () { 22 | counter = counter - 1 23 | if (counter === 0) { 24 | console.log('all tests passed') 25 | router.close() 26 | } 27 | } 28 | 29 | router.httpServer.listen(8080, function () { 30 | counter++ 31 | request('http://localhost:8080/name', {headers:{host:'app1.localhost'}}, function (e, resp) { 32 | assert.ok(!e) 33 | assert.equal(resp.statusCode, 200) 34 | assert.equal(resp.body, 'app1') 35 | end() 36 | }) 37 | 38 | counter++ 39 | request('http://localhost:8080/name', {headers:{host:'app2.localhost'}}, function (e, resp) { 40 | assert.ok(!e) 41 | assert.equal(resp.statusCode, 200) 42 | assert.equal(resp.body, 'app2') 43 | end() 44 | }) 45 | 46 | counter++ 47 | request('http://localhost:8080/name', {headers:{host:'unknown.localhost'}}, function (e, resp) { 48 | assert.ok(!e) 49 | assert.equal(resp.statusCode, 200) 50 | assert.equal(resp.body, 'app3') 51 | end() 52 | }) 53 | }) -------------------------------------------------------------------------------- /rfc822.js: -------------------------------------------------------------------------------- 1 | // Support for rfc822, worst standard EVAR! 2 | // from http://sanctumvoid.net/jsexamples/rfc822datetime/rfc822datetime.html 3 | 4 | function getRFC822Date(oDate) { 5 | var aMonths = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", 6 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"); 7 | 8 | var aDays = new Array( "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); 9 | var dtm = new String(); 10 | 11 | dtm = aDays[oDate.getDay()] + ", "; 12 | dtm += padWithZero(oDate.getDate()) + " "; 13 | dtm += aMonths[oDate.getMonth()] + " "; 14 | dtm += oDate.getFullYear() + " "; 15 | dtm += padWithZero(oDate.getHours()) + ":"; 16 | dtm += padWithZero(oDate.getMinutes()) + ":"; 17 | dtm += padWithZero(oDate.getSeconds()) + " " ; 18 | dtm += getTZOString(oDate.getTimezoneOffset()); 19 | return dtm; 20 | } 21 | //Pads numbers with a preceding 0 if the number is less than 10. 22 | function padWithZero(val) { 23 | if (parseInt(val) < 10) 24 | { 25 | return "0" + val; 26 | } 27 | return val; 28 | } 29 | 30 | /* accepts the client's time zone offset from GMT in minutes as a parameter. 31 | returns the timezone offset in the format [+|-}DDDD */ 32 | function getTZOString(timezoneOffset) { 33 | var hours = Math.floor(timezoneOffset/60); 34 | var modMin = Math.abs(timezoneOffset%60); 35 | var s = new String(); 36 | s += (hours > 0) ? "-" : "+"; 37 | var absHours = Math.abs(hours) 38 | s += (absHours < 10) ? "0" + absHours :absHours; 39 | s += ((modMin == 0) ? "00" : modMin); 40 | return(s); 41 | } 42 | 43 | exports.getRFC822Date = getRFC822Date; 44 | -------------------------------------------------------------------------------- /tests/test-content-types.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , request = require('request') 3 | , common = require('./common') 4 | , tako = require('../') 5 | , app = tako() 6 | 7 | var URL = common.URL 8 | , HTML = '

Hello world

' 9 | 10 | app.route('/hello').json(function (req, res) { 11 | res.end({ hello: 'world' }) 12 | }) 13 | 14 | app.route('/json').json({ hello: 'json' }) 15 | 16 | app.route('/html').html(function (req, res) { 17 | res.end(HTML) 18 | }) 19 | 20 | app.httpServer.listen(common.PORT, function () { 21 | common.all( 22 | [ 23 | [ function (cb) { 24 | request({ url: URL + '/hello', json: true }, cb) 25 | } 26 | , function (err, res, body) { 27 | common.assertStatus(res, 200) 28 | common.assertJSON(res) 29 | assert.deepEqual(body, { hello: 'world' }) 30 | } 31 | ] 32 | , [ function (cb) { 33 | request({ url: URL + '/json', json: true }, cb) 34 | } 35 | , function (err, res, body) { 36 | common.assertStatus(res, 200) 37 | common.assertJSON(res) 38 | assert.deepEqual(body, { hello: 'json' }) 39 | } 40 | ] 41 | , [ function (cb) { 42 | request(URL + '/json', cb) 43 | } 44 | , function (err, res, body) { 45 | common.assertStatus(res, 200) 46 | } 47 | ] 48 | , [ function (cb) { 49 | request( 50 | { url: URL + '/html' 51 | , headers: { 'accept': 'text/html' } 52 | }, cb); 53 | } 54 | , 55 | function (err, res, body) { 56 | common.assertStatus(res, 200) 57 | assert.equal(body, HTML) 58 | } 59 | ] 60 | ] 61 | , function () {app.close()}) 62 | }) 63 | 64 | -------------------------------------------------------------------------------- /tests/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs') 3 | , path = require('path') 4 | , spawn = require('child_process').spawn 5 | 6 | var testTimeout = 8000 7 | , failed = [] 8 | , success = [] 9 | , pathPrefix = __dirname 10 | 11 | function runTest(test, callback) { 12 | var child = spawn(process.execPath, [ path.join(__dirname, test) ]) 13 | , stdout = '' 14 | , stderr = '' 15 | , killTimeout 16 | 17 | child.stdout.on('data', function (chunk) { 18 | stdout += chunk 19 | }) 20 | 21 | child.stderr.on('data', function (chunk) { 22 | stderr += chunk 23 | }) 24 | 25 | killTimeout = setTimeout(function () { 26 | child.kill() 27 | console.log(' ' + path.basename(test) + ' timed out') 28 | callback() 29 | }, testTimeout) 30 | 31 | child.on('exit', function (exitCode) { 32 | clearTimeout(killTimeout) 33 | 34 | console.log(' ' + (exitCode ? '✘' : '✔') + ' ' + path.basename(test)) 35 | ;(exitCode ? failed : success).push(test) 36 | if (exitCode) { 37 | console.log('stdout:') 38 | process.stdout.write(stdout) 39 | 40 | console.log('stderr:') 41 | process.stdout.write(stderr) 42 | } 43 | callback() 44 | }) 45 | } 46 | 47 | function runTests(tests) { 48 | var index = 0 49 | 50 | console.log('Running tests:') 51 | 52 | function next() { 53 | if (index === tests.length - 1) { 54 | console.log() 55 | console.log('Summary:') 56 | console.log(' ' + success.length + '\tpassed tests') 57 | console.log(' ' + failed.length + '\tfailed tests') 58 | process.exit(failed.length) 59 | } 60 | runTest(tests[++index], next) 61 | } 62 | runTest(tests[0], next) 63 | } 64 | 65 | runTests(fs.readdirSync(pathPrefix).filter(function (test) { 66 | return test.substr(0, 5) === 'test-' 67 | })) 68 | 69 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | # tako -- Functional web framework. 2 | 3 | ## Install 4 | 5 |
 6 |   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 0) { 531 | for(var i=0, j=context.length; i": ">", 690 | '"': """, 691 | "'": "'", 692 | "`": "`" 693 | }; 694 | 695 | var badChars = /&(?!\w+;)|[<>"'`]/g; 696 | var possible = /[&<>"'`]/; 697 | 698 | var escapeChar = function(chr) { 699 | return escape[chr] || "&" 700 | }; 701 | 702 | Handlebars.Utils = { 703 | escapeExpression: function(string) { 704 | // don't escape SafeStrings, since they're already safe 705 | if (string instanceof Handlebars.SafeString) { 706 | return string.toString(); 707 | } else if (string == null || string === false) { 708 | return ""; 709 | } 710 | 711 | if(!possible.test(string)) { return string; } 712 | return string.replace(badChars, escapeChar); 713 | }, 714 | 715 | isEmpty: function(value) { 716 | if (typeof value === "undefined") { 717 | return true; 718 | } else if (value === null) { 719 | return true; 720 | } else if (value === false) { 721 | return true; 722 | } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { 723 | return true; 724 | } else { 725 | return false; 726 | } 727 | } 728 | }; 729 | })();; 730 | // lib/handlebars/compiler.js 731 | Handlebars.Compiler = function() {}; 732 | Handlebars.JavaScriptCompiler = function() {}; 733 | 734 | (function(Compiler, JavaScriptCompiler) { 735 | Compiler.OPCODE_MAP = { 736 | appendContent: 1, 737 | getContext: 2, 738 | lookupWithHelpers: 3, 739 | lookup: 4, 740 | append: 5, 741 | invokeMustache: 6, 742 | appendEscaped: 7, 743 | pushString: 8, 744 | truthyOrFallback: 9, 745 | functionOrFallback: 10, 746 | invokeProgram: 11, 747 | invokePartial: 12, 748 | push: 13, 749 | invokeInverse: 14, 750 | assignToHash: 15, 751 | pushStringParam: 16 752 | }; 753 | 754 | Compiler.MULTI_PARAM_OPCODES = { 755 | appendContent: 1, 756 | getContext: 1, 757 | lookupWithHelpers: 1, 758 | lookup: 1, 759 | invokeMustache: 2, 760 | pushString: 1, 761 | truthyOrFallback: 1, 762 | functionOrFallback: 1, 763 | invokeProgram: 2, 764 | invokePartial: 1, 765 | push: 1, 766 | invokeInverse: 1, 767 | assignToHash: 1, 768 | pushStringParam: 1 769 | }; 770 | 771 | Compiler.DISASSEMBLE_MAP = {}; 772 | 773 | for(var prop in Compiler.OPCODE_MAP) { 774 | var value = Compiler.OPCODE_MAP[prop]; 775 | Compiler.DISASSEMBLE_MAP[value] = prop; 776 | } 777 | 778 | Compiler.multiParamSize = function(code) { 779 | return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; 780 | }; 781 | 782 | Compiler.prototype = { 783 | compiler: Compiler, 784 | 785 | disassemble: function() { 786 | var opcodes = this.opcodes, opcode, nextCode; 787 | var out = [], str, name, value; 788 | 789 | for(var i=0, l=opcodes.length; i 0) { 1139 | this.source[0] = this.source[0] + ", " + locals.join(", "); 1140 | } 1141 | 1142 | this.source[0] = this.source[0] + ";"; 1143 | 1144 | this.source.push("return buffer;"); 1145 | 1146 | var params = ["Handlebars", "context", "helpers", "partials"]; 1147 | 1148 | if(this.options.data) { params.push("data"); } 1149 | 1150 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1402 | return "stack" + this.stackSlot; 1403 | }, 1404 | 1405 | popStack: function() { 1406 | return "stack" + this.stackSlot--; 1407 | }, 1408 | 1409 | topStack: function() { 1410 | return "stack" + this.stackSlot; 1411 | }, 1412 | 1413 | quotedString: function(str) { 1414 | return '"' + str 1415 | .replace(/\\/g, '\\\\') 1416 | .replace(/"/g, '\\"') 1417 | .replace(/\n/g, '\\n') 1418 | .replace(/\r/g, '\\r') + '"'; 1419 | } 1420 | }; 1421 | 1422 | var reservedWords = ("break case catch continue default delete do else finally " + 1423 | "for function if in instanceof new return switch this throw " + 1424 | "try typeof var void while with null true false").split(" "); 1425 | 1426 | compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; 1427 | 1428 | for(var i=0, l=reservedWords.length; i