├── src ├── server.coffee ├── project.coffee └── main.coffee ├── bin ├── compiler.jar ├── yuicompressor-2.4.2.jar └── capt ├── templates ├── templates │ ├── template.hbs │ ├── template.eco │ ├── i18n │ │ ├── en_us.json │ │ └── en_ca.json │ └── helpers │ │ └── all.js ├── models │ ├── model.coffee │ ├── model.js │ ├── spec.coffee │ └── spec.js ├── app.js ├── collection │ ├── collection.coffee │ ├── collection.js │ ├── spec.coffee │ └── spec.js ├── routers │ ├── application.coffee │ ├── router.coffee │ ├── ApplicationRouter.js │ └── spec.coffee ├── views │ ├── view.coffee │ ├── ApplicationView.js │ ├── view.js │ ├── spec.coffee │ ├── ApplicationViewSpec.js │ └── spec.js ├── spec.js ├── html │ ├── runner.html │ └── index.html └── lib │ ├── Handlebars │ └── i18nprecompile.js │ ├── jasmine.css │ ├── jasmine-html.js │ ├── hbs.js │ └── underscore.js ├── package.json ├── readme.md └── lib ├── request.js ├── yaml.js ├── router.js ├── parseopt.js └── underscore.js /src/server.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burin/capt/HEAD/bin/compiler.jar -------------------------------------------------------------------------------- /templates/templates/template.hbs: -------------------------------------------------------------------------------- 1 |

<%= view.capitalize() %> Template!

2 | -------------------------------------------------------------------------------- /templates/templates/template.eco: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/yuicompressor-2.4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burin/capt/HEAD/bin/yuicompressor-2.4.2.jar -------------------------------------------------------------------------------- /templates/templates/i18n/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1" : "This is key 1.", 3 | "key2" : "This is key 2." 4 | } -------------------------------------------------------------------------------- /templates/templates/i18n/en_ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1" : "This is key 1, eh?", 3 | "key2" : "This is key 2, eh?" 4 | } -------------------------------------------------------------------------------- /templates/models/model.coffee: -------------------------------------------------------------------------------- 1 | class <%= model.capitalize() %> extends Backbone.Model 2 | initialize: -> 3 | # ... 4 | 5 | @<%= model.capitalize() %> = <%= model.capitalize() %> 6 | -------------------------------------------------------------------------------- /templates/app.js: -------------------------------------------------------------------------------- 1 | require([ 2 | 'jquery', 'backbone', 'routers/ApplicationRouter' 3 | ], 4 | function($, Backbone, ApplicationRouter) { 5 | new ApplicationRouter(); 6 | Backbone.history.start(); 7 | }); 8 | -------------------------------------------------------------------------------- /templates/collection/collection.coffee: -------------------------------------------------------------------------------- 1 | class <%= model.capitalize() %>Collection extends Backbone.Collection 2 | model: <%= model.capitalize() %> 3 | 4 | @<%= model.capitalize() %>Collection = <%= model.capitalize() %>Collection 5 | -------------------------------------------------------------------------------- /templates/models/model.js: -------------------------------------------------------------------------------- 1 | // register as a module with AMD. 2 | define(['backbone'],function(Backbone) { 3 | var <%= model.capitalize() %> = Backbone.Model.extend({ 4 | initialize: function() { 5 | // 6 | } 7 | }); 8 | return <%= model.capitalize() %>; 9 | }); 10 | -------------------------------------------------------------------------------- /templates/routers/application.coffee: -------------------------------------------------------------------------------- 1 | class Application 2 | constructor: -> 3 | 4 | start: -> 5 | console.log 'App started' 6 | 7 | # Create your controllers here... 8 | 9 | # Then start backbone 10 | # Backbone.history.start() 11 | 12 | 13 | @Application = Application 14 | -------------------------------------------------------------------------------- /templates/views/view.coffee: -------------------------------------------------------------------------------- 1 | class <%= controller.capitalize() %><%= view.capitalize() %>View extends Backbone.View 2 | initialize: -> 3 | 4 | render: -> 5 | $(@el).html("blah!") 6 | 7 | @<%= controller.capitalize() %><%= view.capitalize() %>View = <%= controller.capitalize() %><%= view.capitalize() %>View 8 | -------------------------------------------------------------------------------- /bin/capt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | root = __dirname + "/.." 4 | 5 | require("coffee-script") 6 | require(root + "/src/main.coffee") 7 | // 8 | // var path = require('path'); 9 | // var fs = require('fs'); 10 | // var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); 11 | // 12 | // require(lib + '/cake').run(); 13 | -------------------------------------------------------------------------------- /templates/collection/collection.js: -------------------------------------------------------------------------------- 1 | // register as a module with AMD, bring in Model as dependency 2 | define(['backbone', 'models/<%= model.capitalize() %>'], function (Backbone, <%= model.capitalize() %>) { 3 | var <%= model.capitalize() %>Collection = Backbone.Collection.extend({ 4 | model: <%= model.capitalize() %> 5 | }); 6 | return <%= model.capitalize() %>Collection; 7 | }); 8 | 9 | -------------------------------------------------------------------------------- /src/project.coffee: -------------------------------------------------------------------------------- 1 | sys = require('sys') 2 | 3 | sys.puts "Capt:" 4 | 5 | String.prototype.capitalize = -> 6 | this.charAt(0).toUpperCase() + this.substring(1).toLowerCase() 7 | 8 | class Project 9 | constructor: (cwd) -> 10 | @cwd = cwd 11 | @root = cwd 12 | name : -> 13 | @cwd.replace(/.+\//,'') 14 | 15 | language : -> 16 | 'js' # or 'coffee' 17 | 18 | exports.Project = Project 19 | -------------------------------------------------------------------------------- /templates/views/ApplicationView.js: -------------------------------------------------------------------------------- 1 | define(['backbone'], function(Backbone) { 2 | var ApplicationView = Backbone.View.extend({ 3 | initialize: function() { 4 | this.el = $(this.el); 5 | }, 6 | render: function() { 7 | this.el.append('
well hello there!
'); 8 | } 9 | }); 10 | 11 | return ApplicationView; 12 | }); 13 | -------------------------------------------------------------------------------- /templates/models/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= model %> model', -> 2 | 3 | it 'should handle the truth', -> 4 | expect(true).toBeTruthy() 5 | 6 | it 'should exist', -> 7 | expect(<%= model.capitalize() %>).toBeTruthy() 8 | 9 | it 'should instantiate', -> 10 | x = new <%= model.capitalize() %> 11 | expect(x instanceof <%= model.capitalize() %>).toBeTruthy() 12 | expect(x instanceof Backbone.Model).toBeTruthy() 13 | 14 | -------------------------------------------------------------------------------- /templates/routers/router.coffee: -------------------------------------------------------------------------------- 1 | class <%= controller.capitalize() %>Controller extends Backbone.Controller 2 | routes : 3 | "<%= controller %>/:id/edit" : "edit" 4 | "<%= controller %>/new" : "new" 5 | "<%= controller %>/:id" : "show" 6 | "<%= controller %>" : "index" 7 | 8 | index: -> 9 | # new <%= controller.capitalize() %>IndexView 10 | 11 | @<%= controller.capitalize() %>Controller = <%= controller.capitalize() %>Controller 12 | -------------------------------------------------------------------------------- /templates/spec.js: -------------------------------------------------------------------------------- 1 | // baseUrl has been set to ../app so that module paths work properly 2 | // specs should be referenced here as though you were in `app` 3 | // app/ 4 | // spec/ 5 | require([ 6 | // 7 | '../spec/views/ApplicationViewSpec'// last item marker, do not remove :) 8 | ], 9 | function() { 10 | jasmine.getEnv().addReporter(new jasmine.TrivialReporter()); 11 | jasmine.getEnv().execute(); 12 | }); 13 | -------------------------------------------------------------------------------- /templates/collection/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= model %> collection', -> 2 | 3 | it 'should handle the truth', -> 4 | expect(true).toBeTruthy() 5 | 6 | it 'should exist', -> 7 | expect(<%= model.capitalize() %>Collection).toBeTruthy() 8 | 9 | it 'should instantiate', -> 10 | x = new <%= model.capitalize() %>Collection 11 | expect(x instanceof <%= model.capitalize() %>Collection).toBeTruthy() 12 | expect(x instanceof Backbone.Collection).toBeTruthy() 13 | expect(x.model == <%= model.capitalize() %>).toBeTruthy() 14 | 15 | -------------------------------------------------------------------------------- /templates/views/view.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'hbs!templates/<%= router %>/<%= view.capitalize() %>Template'], function(Backbone, template) { 2 | var <%= view.capitalize() %>View = Backbone.View.extend({ 3 | initialize: function() { 4 | this.el = $(this.el); 5 | this.model.bind('change', this.render, this); 6 | }, 7 | render: function() { 8 | this.el.html(this.template(this.model.toJSON())); 9 | }, 10 | template: template 11 | }); 12 | return <%= view.capitalize() %>View; 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /templates/routers/ApplicationRouter.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'views/ApplicationView'], function(Backbone, ApplicationView) { 2 | var ApplicationRouter = Backbone.Router.extend({ 3 | initialize: function() { 4 | this.viewport = new ApplicationView({ 5 | el: $('#app') 6 | }); 7 | }, 8 | routes: { 9 | "" : "main", 10 | "other" : "other" 11 | }, 12 | main: function() { 13 | this.viewport.render(); 14 | }, 15 | other: function() { 16 | this.viewport.render(); 17 | } 18 | }); 19 | 20 | return ApplicationRouter; 21 | }); 22 | -------------------------------------------------------------------------------- /templates/routers/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= controller %> controller', -> 2 | 3 | it 'should handle the truth', -> 4 | expect(true).toBeTruthy() 5 | 6 | it 'should exist', -> 7 | expect(<%= controller.capitalize() %>Controller).toBeTruthy() 8 | 9 | it 'should instantiate', -> 10 | x = new <%= controller.capitalize() %>Controller 11 | expect(x instanceof <%= controller.capitalize() %>Controller).toBeTruthy() 12 | expect(x instanceof Backbone.Controller).toBeTruthy() 13 | 14 | it 'should have index method', -> 15 | x = new <%= controller.capitalize() %>Controller 16 | x.index() 17 | 18 | # Umm..? 19 | expect(true).toBeTruthy() 20 | -------------------------------------------------------------------------------- /templates/models/spec.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'models/<%= model.capitalize() %>'], function(Backbone, <%= model.capitalize() %>) { 2 | describe('<%= model.capitalize() %> model', function() { 3 | it('should handle the truth', function() { 4 | expect(true).toBeTruthy(); 5 | }); 6 | 7 | it('should exist', function() { 8 | expect(<%= model.capitalize() %>).toBeTruthy(); 9 | }); 10 | 11 | it('should instantiate', function() { 12 | var x = new <%= model.capitalize() %>(); 13 | expect(x instanceof <%= model.capitalize() %>).toBeTruthy(); 14 | expect(x instanceof Backbone.Model).toBeTruthy(); 15 | }); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /templates/html/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jasmine Test Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enterprise Mobile Development 5 | 6 | 7 | 8 |
9 |
10 |
11 |

Enterprise Mobile Development

12 |
13 |
14 |

hello world

15 |
16 |
17 |
18 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /templates/collection/spec.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'collections/<%= model.capitalize() %>Collection', 'models/<%= model.capitalize() %>'], function(Backbone, <%= model.capitalize() %>Collection, <%= model.capitalize() %>) { 2 | describe('<%= model.capitalize() %>Collection', function() { 3 | it('should handle the truth', function() { 4 | expect(true).toBeTruthy(); 5 | }); 6 | 7 | it('should exist', function() { 8 | expect(<%= model.capitalize() %>Collection).toBeTruthy(); 9 | }); 10 | 11 | it('should instantiate', function() { 12 | var x = new <%= model.capitalize() %>Collection(); 13 | expect(x instanceof <%= model.capitalize() %>Collection).toBeTruthy(); 14 | expect(x instanceof Backbone.Collection).toBeTruthy(); 15 | expect(x.model === <%= model.capitalize() %>).toBeTruthy() 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/views/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= controller %> controller', -> 2 | 3 | describe '<%= view %> view', -> 4 | 5 | it 'should handle the truth', -> 6 | expect(true).toBeTruthy() 7 | 8 | it 'should exist', -> 9 | expect(<%= controller.capitalize() %><%= view.capitalize() %>View).toBeTruthy() 10 | 11 | it 'should instantiate', -> 12 | x = new <%= controller.capitalize() %><%= view.capitalize() %>View 13 | expect(x instanceof <%= controller.capitalize() %><%= view.capitalize() %>View).toBeTruthy() 14 | expect(x instanceof Backbone.View).toBeTruthy() 15 | 16 | it 'should have render method', -> 17 | x = new <%= controller.capitalize() %><%= view.capitalize() %>View 18 | x.render() 19 | 20 | # Implement as you see fit 21 | xit 'should render some text', -> 22 | x = new <%= controller.capitalize() %><%= view.capitalize() %>View { el : $("
") } 23 | x.render() 24 | expect(x.$(".myselector").html()).toMatch /some text/ 25 | -------------------------------------------------------------------------------- /templates/views/ApplicationViewSpec.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'views/ApplicationView'], function(Backbone, ApplicationView) { 2 | describe('ApplicationView', function() { 3 | it('should handle the truth', function() { 4 | expect(true).toBeTruthy(); 5 | }); 6 | 7 | it('should exist', function() { 8 | expect(ApplicationView).toBeTruthy(); 9 | }); 10 | 11 | it('should instantiate', function() { 12 | var x = new ApplicationView(); 13 | expect(x instanceof ApplicationView).toBeTruthy(); 14 | expect(x instanceof Backbone.View).toBeTruthy(); 15 | }); 16 | 17 | it('should have a render method', function() { 18 | var x = new ApplicationView(); 19 | x.render(); 20 | }); 21 | 22 | it('should render some text', function() { 23 | var x = new ApplicationView(); 24 | x.render(); 25 | expect(x.$(".appended").html()).toMatch(/well hello there!/); 26 | }); 27 | }); 28 | }); 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capt", 3 | "description": "Command line tool for creating backbone.js applications with coffeescript", 4 | "version": "0.5.5", 5 | "keywords": ["jst", "templates", "rest", "backbone", "jquery", "zepto", "framework", "coffeescript", "less", "underscore"], 6 | "homepage": "http://bnolan.github.com/capt/", 7 | "author": "Ben Nolan , twitter: @bnolan", 8 | "licenses": [{ 9 | "type": "MIT", 10 | "url": "http://github.com/brunch/brunch/raw/master/LICENSE" 11 | }], 12 | "directories" : { 13 | "lib" : "./lib" 14 | }, 15 | "main" : "./lib/brunch", 16 | "bin": { 17 | "capt": "./bin/capt" 18 | }, 19 | "dependencies": { 20 | "coffee-script": "= 1.0.1", 21 | "less": ">=1.0.41", 22 | "glob": ">=2.0.6", 23 | "jasmine-node" : ">=1.0.0rc3", 24 | "eco" : ">=1.0.3" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/bnolan/capt.git" 29 | }, 30 | "bugs": { 31 | "url": "http://github.com/bnolan/capt/issues" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/views/spec.js: -------------------------------------------------------------------------------- 1 | define(['backbone', 'views/<%= router %>/<%= view.capitalize() %>View'], function(Backbone, <%= view.capitalize() %>View) { 2 | describe('<%= router.capitalize() %> <%= view.capitalize() %>View', function() { 3 | it('should handle the truth', function() { 4 | expect(true).toBeTruthy(); 5 | }); 6 | 7 | it('should exist', function() { 8 | expect(<%= view.capitalize() %>View).toBeTruthy(); 9 | }); 10 | 11 | it('should instantiate', function() { 12 | var x = new <%= view.capitalize() %>View({ 13 | model: new Backbone.Model() 14 | }); 15 | expect(x instanceof <%= view.capitalize() %>View).toBeTruthy(); 16 | expect(x instanceof Backbone.View).toBeTruthy(); 17 | }); 18 | 19 | it('should have a render method', function() { 20 | var x = new <%= view.capitalize() %>View({ 21 | model: new Backbone.Model() 22 | }); 23 | x.render(); 24 | }); 25 | 26 | it('should render some text', function() { 27 | var x = new <%= view.capitalize() %>View({ 28 | model: new Backbone.Model() 29 | }); 30 | x.render(); 31 | expect(x.el.html()).toMatch('<%= view.capitalize() %> Template!'); 32 | }); 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /templates/templates/helpers/all.js: -------------------------------------------------------------------------------- 1 | define(['Handlebars'], function(Handlebars) { 2 | function ISODateString(d) { 3 | function pad(n){ 4 | return n < 10 ? '0'+n : n 5 | } 6 | return d.getUTCFullYear()+'-' 7 | + pad(d.getUTCMonth()+1)+'-' 8 | + pad(d.getUTCDate())+'T' 9 | + pad(d.getUTCHours())+':' 10 | + pad(d.getUTCMinutes())+':' 11 | + pad(d.getUTCSeconds())+'Z' 12 | } 13 | Handlebars.registerHelper('timeSimple', function(date) { 14 | return moment(date).format('hh:mma'); 15 | }); 16 | 17 | Handlebars.registerHelper('prettyDate', function(date) { 18 | var hehe = moment(date).format('ddd MMM D, YYYY'); 19 | return hehe; 20 | }); 21 | 22 | Handlebars.registerHelper('toLowerCase', function(string) { 23 | return string.toLowerCase(); 24 | }); 25 | 26 | Handlebars.registerHelper('carrierIconTag', function(carrierCode) { 27 | // escape carrier code 28 | carrierCode = Handlebars.Utils.escapeExpression(carrierCode); 29 | // concat image tag with carrier code 30 | var img = '' + carrierCode.toLowerCase() + 'logo'; 31 | // output safestring 32 | return new Handlebars.SafeString(img); 33 | }); 34 | 35 | Handlebars.registerHelper('formatPrice', function(price) { 36 | return parseFloat((price)*100/100).toFixed(2); 37 | }); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /templates/lib/Handlebars/i18nprecompile.js: -------------------------------------------------------------------------------- 1 | //>>excludeStart('excludeAfterBuild', pragmas.excludeAfterBuild) 2 | define(['Handlebars', "underscore"], function ( Handlebars, _ ) { 3 | // define(function () { 4 | function replaceLocaleStrings ( ast, mapping ) { 5 | mapping = mapping || {}; 6 | // Base set of things 7 | if ( ast && ast.type === "program" && ast.statements ) { 8 | _(ast.statements).forEach(function(statement, i){ 9 | var newString = ""; 10 | // If it's a translation node 11 | if ( statement.type == "mustache" && statement.id && statement.id.original == "$" ) { 12 | 13 | if ( statement.params.length && statement.params[0].string ) { 14 | newString = mapping[ statement.params[0].string ] || newString; 15 | } 16 | ast.statements[i] = new Handlebars.AST.ContentNode(newString); 17 | } 18 | // If we need to recurse 19 | else if ( statement.program ) { 20 | statement.program = replaceLocaleStrings( statement.program, mapping ); 21 | } 22 | }); 23 | } 24 | return ast; 25 | } 26 | 27 | return function(string, mapping, options) { 28 | options = options || {}; 29 | var ast = replaceLocaleStrings(Handlebars.parse(string), mapping), 30 | environment = new Handlebars.Compiler().compile(ast, options); 31 | return new Handlebars.JavaScriptCompiler().compile(environment, options); 32 | }; 33 | }); 34 | //>>excludeEnd('excludeAfterBuild') 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Backbone environment for node.js (capt) 2 | 3 | `capt` is a tool to help you quickly create backbone.js applications and maintain a good directory structure and give you build tools to help development. 4 | 5 | This version of `capt` only includes the `new` and `generate` tasks. 6 | 7 | It assumes the project will use [RequireJS](http://requirejs.org/) for module loading and dependency management. JavaScript is the preferred language in this fork. 8 | 9 | 10 | Directory Structure: 11 | 12 | index.html 13 | app/ 14 | app.js 15 | collections/ 16 | models/ 17 | routers/ 18 | templates/ 19 | views/ 20 | css/ 21 | lib/ 22 | spec/ 23 | 24 | Testing framework: 25 | 26 | * Jasmine 27 | 28 | Libraries built in (as AMD modules): 29 | 30 | * jQuery 31 | * backbone.js 32 | * underscore.js 33 | * require.js 34 | * handlebars.js 35 | * require-handlebars-plugin (https://github.com/SlexAxton/require-handlebars-plugin) 36 | 37 | ## Getting Started 38 | Create a new project by 39 | 40 | capt new projectname 41 | 42 | Open `projectname/index.html` to see your initial app 43 | 44 | Then you can `cd` into the directory `projectname` to start using generators 45 | 46 | cd projectname 47 | 48 | Generate a model 49 | 50 | capt generate model animal 51 | 52 | Generate a collection with the model name 53 | 54 | capt generate collection animal 55 | 56 | Or generate some views with the directory name and view name 57 | 58 | capt generate view animals show 59 | 60 | 61 | 62 | ## License 63 | 64 | [BSD Licensed](http://creativecommons.org/licenses/BSD/). YUI Compressor and Closure are licensed under their respective licences. 65 | 66 | ## Author 67 | 68 | Ben Nolan @bnolan bnolan@gmail.com 69 | 70 | Modified by Burin Asavesna @burin 71 | 72 | # Changelog 73 | 74 | 0.5.4 - Fixed template which said `initailizer` instead of `initialize` -------------------------------------------------------------------------------- /templates/lib/jasmine.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; 3 | } 4 | 5 | 6 | .jasmine_reporter a:visited, .jasmine_reporter a { 7 | color: #303; 8 | } 9 | 10 | .jasmine_reporter a:hover, .jasmine_reporter a:active { 11 | color: blue; 12 | } 13 | 14 | .run_spec { 15 | float:right; 16 | padding-right: 5px; 17 | font-size: .8em; 18 | text-decoration: none; 19 | } 20 | 21 | .jasmine_reporter { 22 | margin: 0 5px; 23 | } 24 | 25 | .banner { 26 | color: #303; 27 | background-color: #fef; 28 | padding: 5px; 29 | } 30 | 31 | .logo { 32 | float: left; 33 | font-size: 1.1em; 34 | padding-left: 5px; 35 | } 36 | 37 | .logo .version { 38 | font-size: .6em; 39 | padding-left: 1em; 40 | } 41 | 42 | .runner.running { 43 | background-color: yellow; 44 | } 45 | 46 | 47 | .options { 48 | text-align: right; 49 | font-size: .8em; 50 | } 51 | 52 | 53 | 54 | 55 | .suite { 56 | border: 1px outset gray; 57 | margin: 5px 0; 58 | padding-left: 1em; 59 | } 60 | 61 | .suite .suite { 62 | margin: 5px; 63 | } 64 | 65 | .suite.passed { 66 | background-color: #dfd; 67 | } 68 | 69 | .suite.failed { 70 | background-color: #fdd; 71 | } 72 | 73 | .spec { 74 | margin: 5px; 75 | padding-left: 1em; 76 | clear: both; 77 | } 78 | 79 | .spec.failed, .spec.passed, .spec.skipped { 80 | padding-bottom: 5px; 81 | border: 1px solid gray; 82 | } 83 | 84 | .spec.failed { 85 | background-color: #fbb; 86 | border-color: red; 87 | } 88 | 89 | .spec.passed { 90 | background-color: #bfb; 91 | border-color: green; 92 | } 93 | 94 | .spec.skipped { 95 | background-color: #bbb; 96 | } 97 | 98 | .messages { 99 | border-left: 1px dashed gray; 100 | padding-left: 1em; 101 | padding-right: 1em; 102 | } 103 | 104 | .passed { 105 | background-color: #cfc; 106 | display: none; 107 | } 108 | 109 | .failed { 110 | background-color: #fbb; 111 | } 112 | 113 | .skipped { 114 | color: #777; 115 | background-color: #eee; 116 | display: none; 117 | } 118 | 119 | 120 | /*.resultMessage {*/ 121 | /*white-space: pre;*/ 122 | /*}*/ 123 | 124 | .resultMessage span.result { 125 | display: block; 126 | line-height: 2em; 127 | color: black; 128 | } 129 | 130 | .resultMessage .mismatch { 131 | color: black; 132 | } 133 | 134 | .stackTrace { 135 | white-space: pre; 136 | font-size: .8em; 137 | margin-left: 10px; 138 | max-height: 5em; 139 | overflow: auto; 140 | border: 1px inset red; 141 | padding: 1em; 142 | background: #eef; 143 | } 144 | 145 | .finished-at { 146 | padding-left: 1em; 147 | font-size: .6em; 148 | } 149 | 150 | .show-passed .passed, 151 | .show-skipped .skipped { 152 | display: block; 153 | } 154 | 155 | 156 | #jasmine_content { 157 | position:fixed; 158 | right: 100%; 159 | } 160 | 161 | .runner { 162 | border: 1px solid gray; 163 | display: block; 164 | margin: 5px 0; 165 | padding: 2px 0 2px 10px; 166 | } 167 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , url = require('url') 3 | , sys = require('sys') 4 | ; 5 | 6 | var toBase64 = function(str) { 7 | return (new Buffer(str || "", "ascii")).toString("base64"); 8 | }; 9 | 10 | function request (options, callback) { 11 | if (!options.uri) { 12 | throw new Error("options.uri is a required argument") 13 | } else { 14 | if (typeof options.uri == "string") options.uri = url.parse(options.uri); 15 | } 16 | if (options.proxy) { 17 | if (typeof options.proxy == 'string') options.proxy = url.parse(options.proxy); 18 | } 19 | 20 | options._redirectsFollowed = options._redirectsFollowed ? options._redirectsFollowed : 0; 21 | options.maxRedirects = options.maxRedirects ? options.maxRedirects : 10; 22 | 23 | options.followRedirect = (options.followRedirect !== undefined) ? options.followRedirect : true; 24 | options.method = options.method ? options.method : 'GET'; 25 | 26 | options.headers = options.headers ? options.headers : {}; 27 | if (!options.headers.host) { 28 | options.headers.host = options.uri.hostname; 29 | if (options.uri.port) { 30 | if ( !(options.uri.port === 80 && options.uri.protocol === 'http:') && 31 | !(options.uri.port === 443 && options.uri.protocol === 'https:') ) 32 | options.headers.host += (':'+options.uri.port) 33 | } 34 | var setHost = true; 35 | } else { 36 | var setHost = false; 37 | } 38 | 39 | if (!options.uri.pathname) {options.uri.pathname = '/'} 40 | if (!options.uri.port) { 41 | if (options.uri.protocol == 'http:') {options.uri.port = 80} 42 | else if (options.uri.protocol == 'https:') {options.uri.port = 443} 43 | } 44 | 45 | if (options.bodyStream) { 46 | sys.error('options.bodyStream is deprecated. use options.reponseBodyStream instead.'); 47 | options.responseBodyStream = options.bodyStream; 48 | } 49 | if (options.proxy) { 50 | var secure = (options.proxy.protocol == 'https:') ? true : false 51 | options.client = options.client ? options.client : http.createClient(options.proxy.port, options.proxy.hostname, secure); 52 | } else { 53 | var secure = (options.uri.protocol == 'https:') ? true : false 54 | options.client = options.client ? options.client : http.createClient(options.uri.port, options.uri.hostname, secure); 55 | } 56 | 57 | var clientErrorHandler = function (error) { 58 | if (setHost) delete options.headers.host; 59 | if (callback) callback(error); 60 | } 61 | options.client.addListener('error', clientErrorHandler); 62 | 63 | if (options.uri.auth && !options.headers.authorization) { 64 | options.headers.authorization = "Basic " + toBase64(options.uri.auth); 65 | } 66 | if (options.proxy && options.proxy.auth && !options.headers['proxy-authorization']) { 67 | options.headers['proxy-authorization'] = "Basic " + toBase64(options.proxy.auth); 68 | } 69 | 70 | options.fullpath = options.uri.href.replace(options.uri.protocol + '//' + options.uri.host, ''); 71 | if (options.fullpath.length === 0) options.fullpath = '/' 72 | 73 | if (options.proxy) options.fullpath = (options.uri.protocol + '//' + options.uri.host + options.fullpath) 74 | 75 | if (options.body) {options.headers['content-length'] = options.body.length} 76 | options.request = options.client.request(options.method, options.fullpath, options.headers); 77 | options.request.addListener("response", function (response) { 78 | var buffer; 79 | if (options.responseBodyStream) { 80 | buffer = options.responseBodyStream; 81 | sys.pump(response, options.responseBodyStream); 82 | } 83 | else { 84 | buffer = ''; 85 | response.addListener("data", function (chunk) { buffer += chunk; } ) 86 | } 87 | 88 | response.addListener("end", function () { 89 | options.client.removeListener("error", clientErrorHandler); 90 | 91 | if (response.statusCode > 299 && response.statusCode < 400 && options.followRedirect && response.headers.location && (options._redirectsFollowed < options.maxRedirects) ) { 92 | options._redirectsFollowed += 1 93 | options.uri = response.headers.location; 94 | delete options.client; 95 | if (options.headers) { 96 | delete options.headers.host; 97 | } 98 | request(options, callback); 99 | return; 100 | } else {options._redirectsFollowed = 0} 101 | 102 | if (setHost) delete options.headers.host; 103 | if (callback) callback(null, response, buffer); 104 | }) 105 | }) 106 | 107 | if (options.body) { 108 | options.request.write(options.body, 'binary'); 109 | options.request.end(); 110 | } else if (options.requestBodyStream) { 111 | sys.pump(options.requestBodyStream, options.request); 112 | } else { 113 | options.request.end(); 114 | } 115 | } 116 | 117 | module.exports = request; 118 | 119 | request.get = request; 120 | request.post = function () {arguments[0].method = 'POST', request.apply(request, arguments)}; 121 | request.put = function () {arguments[0].method = 'PUT', request.apply(request, arguments)}; 122 | request.head = function () {arguments[0].method = 'HEAD', request.apply(request, arguments)}; -------------------------------------------------------------------------------- /templates/lib/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.TrivialReporter = function(doc) { 2 | this.document = doc || document; 3 | this.suiteDivs = {}; 4 | this.logRunningSpecs = false; 5 | }; 6 | 7 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 8 | var el = document.createElement(type); 9 | 10 | for (var i = 2; i < arguments.length; i++) { 11 | var child = arguments[i]; 12 | 13 | if (typeof child === 'string') { 14 | el.appendChild(document.createTextNode(child)); 15 | } else { 16 | if (child) { el.appendChild(child); } 17 | } 18 | } 19 | 20 | for (var attr in attrs) { 21 | if (attr == "className") { 22 | el[attr] = attrs[attr]; 23 | } else { 24 | el.setAttribute(attr, attrs[attr]); 25 | } 26 | } 27 | 28 | return el; 29 | }; 30 | 31 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 32 | var showPassed, showSkipped; 33 | 34 | this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, 35 | this.createDom('div', { className: 'banner' }, 36 | this.createDom('div', { className: 'logo' }, 37 | this.createDom('a', { href: 'http://pivotal.github.com/jasmine/', target: "_blank" }, "Jasmine"), 38 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 39 | this.createDom('div', { className: 'options' }, 40 | "Show ", 41 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 42 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 43 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 44 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 45 | ) 46 | ), 47 | 48 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 49 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 50 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 51 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 52 | ); 53 | 54 | this.document.body.appendChild(this.outerDiv); 55 | 56 | var suites = runner.suites(); 57 | for (var i = 0; i < suites.length; i++) { 58 | var suite = suites[i]; 59 | var suiteDiv = this.createDom('div', { className: 'suite' }, 60 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 61 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 62 | this.suiteDivs[suite.id] = suiteDiv; 63 | var parentDiv = this.outerDiv; 64 | if (suite.parentSuite) { 65 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 66 | } 67 | parentDiv.appendChild(suiteDiv); 68 | } 69 | 70 | this.startedAt = new Date(); 71 | 72 | var self = this; 73 | showPassed.onclick = function(evt) { 74 | if (showPassed.checked) { 75 | self.outerDiv.className += ' show-passed'; 76 | } else { 77 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 78 | } 79 | }; 80 | 81 | showSkipped.onclick = function(evt) { 82 | if (showSkipped.checked) { 83 | self.outerDiv.className += ' show-skipped'; 84 | } else { 85 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 86 | } 87 | }; 88 | }; 89 | 90 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 91 | var results = runner.results(); 92 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 93 | this.runnerDiv.setAttribute("class", className); 94 | //do it twice for IE 95 | this.runnerDiv.setAttribute("className", className); 96 | var specs = runner.specs(); 97 | var specCount = 0; 98 | for (var i = 0; i < specs.length; i++) { 99 | if (this.specFilter(specs[i])) { 100 | specCount++; 101 | } 102 | } 103 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 104 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 105 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 106 | 107 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 108 | }; 109 | 110 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 111 | var results = suite.results(); 112 | var status = results.passed() ? 'passed' : 'failed'; 113 | if (results.totalCount === 0) { // todo: change this to check results.skipped 114 | status = 'skipped'; 115 | } 116 | this.suiteDivs[suite.id].className += " " + status; 117 | }; 118 | 119 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 120 | if (this.logRunningSpecs) { 121 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 122 | } 123 | }; 124 | 125 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 126 | var results = spec.results(); 127 | var status = results.passed() ? 'passed' : 'failed'; 128 | if (results.skipped) { 129 | status = 'skipped'; 130 | } 131 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 132 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 133 | this.createDom('a', { 134 | className: 'description', 135 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 136 | title: spec.getFullName() 137 | }, spec.description)); 138 | 139 | 140 | var resultItems = results.getItems(); 141 | var messagesDiv = this.createDom('div', { className: 'messages' }); 142 | for (var i = 0; i < resultItems.length; i++) { 143 | var result = resultItems[i]; 144 | 145 | if (result.type == 'log') { 146 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 147 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 148 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 149 | 150 | if (result.trace.stack) { 151 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 152 | } 153 | } 154 | } 155 | 156 | if (messagesDiv.childNodes.length > 0) { 157 | specDiv.appendChild(messagesDiv); 158 | } 159 | 160 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 161 | }; 162 | 163 | jasmine.TrivialReporter.prototype.log = function() { 164 | var console = jasmine.getGlobal().console; 165 | if (console && console.log) { 166 | if (console.log.apply) { 167 | console.log.apply(console, arguments); 168 | } else { 169 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 170 | } 171 | } 172 | }; 173 | 174 | jasmine.TrivialReporter.prototype.getLocation = function() { 175 | return this.document.location; 176 | }; 177 | 178 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 179 | var paramMap = {}; 180 | var params = this.getLocation().search.substring(1).split('&'); 181 | for (var i = 0; i < params.length; i++) { 182 | var p = params[i].split('='); 183 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 184 | } 185 | 186 | if (!paramMap.spec) { 187 | return true; 188 | } 189 | return spec.getFullName().indexOf(paramMap.spec) === 0; 190 | }; -------------------------------------------------------------------------------- /lib/yaml.js: -------------------------------------------------------------------------------- 1 | 2 | // YAML - Core - Copyright TJ Holowaychuk (MIT Licensed) 3 | 4 | /** 5 | * Version triplet. 6 | */ 7 | 8 | exports.version = '0.1.0' 9 | 10 | // --- Helpers 11 | 12 | /** 13 | * Return 'near "context"' where context 14 | * is replaced by a chunk of _str_. 15 | * 16 | * @param {string} str 17 | * @return {string} 18 | * @api public 19 | */ 20 | 21 | function context(str) { 22 | if (typeof str !== 'string') return '' 23 | str = str 24 | .slice(0, 25) 25 | .replace(/\n/g, '\\n') 26 | .replace(/"/g, '\\\"') 27 | return 'near "' + str + '"' 28 | } 29 | 30 | // --- Lexer 31 | 32 | /** 33 | * YAML grammar tokens. 34 | */ 35 | 36 | var tokens = [ 37 | ['comment', /^#[^\n]*/], 38 | ['indent', /^\n( *)/], 39 | ['space', /^ +/], 40 | ['true', /^(enabled|true|yes|on)/], 41 | ['false', /^(disabled|false|no|off)/], 42 | ['string', /^"(.*?)"/], 43 | ['string', /^'(.*?)'/], 44 | ['float', /^(\d+\.\d+)/], 45 | ['int', /^(\d+)/], 46 | ['id', /^([\w ]+)/], 47 | ['doc', /^---/], 48 | [',', /^,/], 49 | ['{', /^\{/], 50 | ['}', /^\}/], 51 | ['[', /^\[/], 52 | [']', /^\]/], 53 | ['-', /^\-/], 54 | [':', /^[:]/], 55 | ] 56 | 57 | /** 58 | * Tokenize the given _str_. 59 | * 60 | * @param {string} str 61 | * @return {array} 62 | * @api private 63 | */ 64 | 65 | exports.tokenize = function (str) { 66 | var token, captures, ignore, input, 67 | indents = lastIndents = 0, 68 | stack = [] 69 | while (str.length) { 70 | for (var i = 0, len = tokens.length; i < len; ++i) 71 | if (captures = tokens[i][1].exec(str)) { 72 | token = [tokens[i][0], captures], 73 | str = str.replace(tokens[i][1], '') 74 | switch (token[0]) { 75 | case 'comment': 76 | ignore = true 77 | break 78 | case 'indent': 79 | lastIndents = indents 80 | indents = token[1][1].length / 2 81 | if (indents === lastIndents) 82 | ignore = true 83 | else if (indents > lastIndents + 1) 84 | throw new SyntaxError('invalid indentation, got ' + indents + ' instead of ' + (lastIndents + 1)) 85 | else if (indents < lastIndents) { 86 | input = token[1].input 87 | token = ['dedent'] 88 | token.input = input 89 | while (--lastIndents > 0) 90 | stack.push(token) 91 | } 92 | } 93 | break 94 | } 95 | if (!ignore) 96 | if (token) 97 | stack.push(token), 98 | token = null 99 | else 100 | throw new SyntaxError(context(str)) 101 | ignore = false 102 | } 103 | return stack 104 | } 105 | 106 | // --- Parser 107 | 108 | /** 109 | * Initialize with _tokens_. 110 | */ 111 | 112 | function Parser(tokens) { 113 | this.tokens = tokens 114 | } 115 | 116 | /** 117 | * Look-ahead a single token. 118 | * 119 | * @return {array} 120 | * @api public 121 | */ 122 | 123 | Parser.prototype.peek = function() { 124 | return this.tokens[0] 125 | } 126 | 127 | /** 128 | * Advance by a single token. 129 | * 130 | * @return {array} 131 | * @api public 132 | */ 133 | 134 | Parser.prototype.advance = function() { 135 | return this.tokens.shift() 136 | } 137 | 138 | /** 139 | * Advance and return the token's value. 140 | * 141 | * @return {mixed} 142 | * @api private 143 | */ 144 | 145 | Parser.prototype.advanceValue = function() { 146 | return this.advance()[1][1] 147 | } 148 | 149 | /** 150 | * Accept _type_ and advance or do nothing. 151 | * 152 | * @param {string} type 153 | * @return {bool} 154 | * @api private 155 | */ 156 | 157 | Parser.prototype.accept = function(type) { 158 | if (this.peekType(type)) 159 | return this.advance() 160 | } 161 | 162 | /** 163 | * Expect _type_ or throw an error _msg_. 164 | * 165 | * @param {string} type 166 | * @param {string} msg 167 | * @api private 168 | */ 169 | 170 | Parser.prototype.expect = function(type, msg) { 171 | if (this.accept(type)) return 172 | throw new Error(msg + ', ' + context(this.peek()[1].input)) 173 | } 174 | 175 | /** 176 | * Return the next token type. 177 | * 178 | * @return {string} 179 | * @api private 180 | */ 181 | 182 | Parser.prototype.peekType = function(val) { 183 | return this.tokens[0] && 184 | this.tokens[0][0] === val 185 | } 186 | 187 | /** 188 | * space* 189 | */ 190 | 191 | Parser.prototype.ignoreSpace = function() { 192 | while (this.peekType('space')) 193 | this.advance() 194 | } 195 | 196 | /** 197 | * (space | indent | dedent)* 198 | */ 199 | 200 | Parser.prototype.ignoreWhitespace = function() { 201 | while (this.peekType('space') || 202 | this.peekType('indent') || 203 | this.peekType('dedent')) 204 | this.advance() 205 | } 206 | 207 | /** 208 | * block 209 | * | doc 210 | * | list 211 | * | inlineList 212 | * | hash 213 | * | inlineHash 214 | * | string 215 | * | float 216 | * | int 217 | * | true 218 | * | false 219 | */ 220 | 221 | Parser.prototype.parse = function() { 222 | switch (this.peek()[0]) { 223 | case 'doc': 224 | return this.parseDoc() 225 | case '-': 226 | return this.parseList() 227 | case '{': 228 | return this.parseInlineHash() 229 | case '[': 230 | return this.parseInlineList() 231 | case 'id': 232 | return this.parseHash() 233 | case 'string': 234 | return this.advanceValue() 235 | case 'float': 236 | return parseFloat(this.advanceValue()) 237 | case 'int': 238 | return parseInt(this.advanceValue()) 239 | case 'true': 240 | return true 241 | case 'false': 242 | return false 243 | } 244 | } 245 | 246 | /** 247 | * '---'? indent expr dedent 248 | */ 249 | 250 | Parser.prototype.parseDoc = function() { 251 | this.accept('doc') 252 | this.expect('indent', 'expected indent after document') 253 | var val = this.parse() 254 | this.expect('dedent', 'document not properly dedented') 255 | return val 256 | } 257 | 258 | /** 259 | * ( id ':' - expr - 260 | * | id ':' - indent expr dedent 261 | * )+ 262 | */ 263 | 264 | Parser.prototype.parseHash = function() { 265 | var id, hash = {} 266 | while (this.peekType('id') && (id = this.advanceValue())) { 267 | this.expect(':', 'expected semi-colon after id') 268 | this.ignoreSpace() 269 | if (this.accept('indent')) 270 | hash[id] = this.parse(), 271 | this.expect('dedent', 'hash not properly dedented') 272 | else 273 | hash[id] = this.parse() 274 | this.ignoreSpace() 275 | } 276 | return hash 277 | } 278 | 279 | /** 280 | * '{' (- ','? ws id ':' - expr ws)* '}' 281 | */ 282 | 283 | Parser.prototype.parseInlineHash = function() { 284 | var hash = {}, id, i = 0 285 | this.accept('{') 286 | while (!this.accept('}')) { 287 | this.ignoreSpace() 288 | if (i) this.expect(',', 'expected comma') 289 | this.ignoreWhitespace() 290 | if (this.peekType('id') && (id = this.advanceValue())) { 291 | this.expect(':', 'expected semi-colon after id') 292 | this.ignoreSpace() 293 | hash[id] = this.parse() 294 | this.ignoreWhitespace() 295 | } 296 | ++i 297 | } 298 | return hash 299 | } 300 | 301 | /** 302 | * ( '-' - expr - 303 | * | '-' - indent expr dedent 304 | * )+ 305 | */ 306 | 307 | Parser.prototype.parseList = function() { 308 | var list = [] 309 | while (this.accept('-')) { 310 | this.ignoreSpace() 311 | if (this.accept('indent')) 312 | list.push(this.parse()), 313 | this.expect('dedent', 'list item not properly dedented') 314 | else 315 | list.push(this.parse()) 316 | this.ignoreSpace() 317 | } 318 | return list 319 | } 320 | 321 | /** 322 | * '[' (- ','? - expr -)* ']' 323 | */ 324 | 325 | Parser.prototype.parseInlineList = function() { 326 | var list = [], i = 0 327 | this.accept('[') 328 | while (!this.accept(']')) { 329 | this.ignoreSpace() 330 | if (i) this.expect(',', 'expected comma') 331 | this.ignoreSpace() 332 | list.push(this.parse()) 333 | this.ignoreSpace() 334 | ++i 335 | } 336 | return list 337 | } 338 | 339 | /** 340 | * Evaluate a _str_ of yaml. 341 | * 342 | * @param {string} str 343 | * @return {mixed} 344 | * @api public 345 | */ 346 | 347 | exports.eval = function(str) { 348 | return (new Parser(exports.tokenize(str))).parse() 349 | } 350 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | sys = require('sys') 3 | Path = require("path") 4 | 5 | root = __dirname + "/../" 6 | router = require("#{root}/lib/router") 7 | request = require("#{root}/lib/request") 8 | _ = require("#{root}/lib/underscore") 9 | server = router.getServer() 10 | Project = require("#{root}/src/project").Project 11 | 12 | String.prototype.capitalize = -> 13 | this.charAt(0).toUpperCase() + this.substring(1).toLowerCase() 14 | 15 | OptionParser = require("#{root}/lib/parseopt").OptionParser 16 | parser = new OptionParser { 17 | minargs : 0 18 | maxargs : 10 19 | } 20 | 21 | $usage = ''' 22 | Usage: 23 | 24 | capt new projectname 25 | - create a new project 26 | 27 | Code generators: 28 | * capt generate model post 29 | * capt generate collection post 30 | * capt generate router posts 31 | * capt generate view posts show 32 | 33 | 34 | ''' 35 | 36 | # Parse command line 37 | data = parser.parse() 38 | 39 | # 40 | # Raise an error 41 | # 42 | raise = (error) -> 43 | sys.puts error 44 | process.exit() 45 | 46 | task = (command, description, func) -> 47 | length = command.split(' ').length 48 | 49 | if data.arguments.slice(0,length).join(" ") == command 50 | func(data.arguments.slice(length)) 51 | task.done = true 52 | 53 | 54 | task 'new', 'create a new project', (arguments) -> 55 | project = arguments[0] or raise("Must supply a name for new project.") 56 | 57 | sys.puts " * Creating folders" 58 | 59 | dirs = ["", "spec", "spec/jasmine", "spec/models", "spec/collections", "spec/routers", "spec/views", "spec/fixtures", 60 | "app", "app/views", "app/templates", "app/templates/helpers", "app/templates/i18n","app/collections", "app/models", "app/routers", 61 | "lib", "lib/Handlebars", "css"] 62 | 63 | for dir in dirs 64 | fs.mkdirSync "#{project}/#{dir}", 0755 65 | 66 | sys.puts " * Creating directory structure" 67 | 68 | libs = { 69 | "lib/require-jquery.js" : "lib/require-jquery.js" 70 | "lib/underscore.js" : "lib/underscore.js" 71 | "lib/backbone.js" : "lib/backbone.js" 72 | "lib/handlebars.js" : "lib/handlebars.js" 73 | "lib/hbs.js" : "lib/hbs.js" 74 | "lib/Handlebars/i18nprecompile.js" : "lib/Handlebars/i18nprecompile.js" 75 | "index.html" : "html/index.html" 76 | "css/bootstrap.css" : "css/bootstrap.css" 77 | "app/app.js" : "app.js" 78 | "app/routers/ApplicationRouter.js" : "routers/ApplicationRouter.js" 79 | "app/views/ApplicationView.js" : "views/ApplicationView.js" 80 | "app/templates/helpers/all.js" : "templates/helpers/all.js" 81 | "app/templates/i18n/en_us.json" : "templates/i18n/en_us.json" 82 | "app/templates/i18n/en_ca.json" : "templates/i18n/en_ca.json" 83 | "spec/index.html" : "html/runner.html" 84 | "spec/spec.js" : "spec.js" 85 | "spec/views/ApplicationViewSpec.js" : "views/ApplicationViewSpec.js" 86 | "spec/jasmine/jasmine-html.js" : "lib/jasmine-html.js" 87 | "spec/jasmine/jasmine.css" : "lib/jasmine.css" 88 | "spec/jasmine/jasmine.js" : "lib/jasmine.js" 89 | } 90 | 91 | downloadLibrary = (path, lib) -> 92 | request { uri : lib }, (error, response, body) -> 93 | if (!error && response.statusCode == 200) 94 | sys.puts " * " + Path.basename(path) 95 | fs.writeFileSync("#{project}/#{path}", body) 96 | else 97 | sys.puts " * [ERROR] Could not download " + Path.basename(path) 98 | 99 | copyLibrary = (path, lib) -> 100 | fs.writeFileSync(Path.join(project, path), fs.readFileSync(lib) + "") 101 | 102 | for path, lib of libs 103 | if lib.match(/^http/) 104 | downloadLibrary(path, lib) 105 | else 106 | copyLibrary(path, Path.join(root, "templates/", lib)) 107 | 108 | task 'generate model', 'create a new model', (arguments) -> 109 | project = new Project(process.cwd()) 110 | 111 | if arguments[0] 112 | model = arguments[0].toLowerCase() 113 | else 114 | raise("Must supply a name for the model") 115 | 116 | appendToSpecRunner = (path) -> 117 | specs = fs.readFileSync(Path.join(project.root, 'spec/spec.js')) + "" 118 | marker = '// ' 119 | newSpecs = specs.split(marker) 120 | newSpecs.splice(1,0,",\n '#{path}'#{marker}") 121 | newSpecs = newSpecs.join('') 122 | 123 | fs.writeFileSync(Path.join(project.root, 'spec/spec.js'), newSpecs) 124 | 125 | copyFile = (from, to) -> 126 | ejs = fs.readFileSync(from) + "" 127 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, model : model })) 128 | sys.puts " * Created #{to}" 129 | 130 | copyFile "#{root}/templates/models/model.#{project.language()}", "app/models/#{model.capitalize()}.#{project.language()}" 131 | copyFile "#{root}/templates/models/spec.#{project.language()}", "spec/models/#{model.capitalize()}Spec.#{project.language()}" 132 | 133 | appendToSpecRunner "../spec/models/#{model.capitalize()}Spec" 134 | 135 | 136 | task 'generate collection', 'create a new collection', (arguments) -> 137 | project = new Project(process.cwd()) 138 | 139 | if arguments[0] 140 | model = arguments[0].toLowerCase() 141 | else 142 | raise("Must supply a name for the model") 143 | 144 | appendToSpecRunner = (path) -> 145 | specs = fs.readFileSync(Path.join(project.root, 'spec/spec.js')) + "" 146 | marker = '// ' 147 | newSpecs = specs.split(marker) 148 | newSpecs.splice(1,0,",\n '#{path}'#{marker}") 149 | newSpecs = newSpecs.join('') 150 | 151 | fs.writeFileSync(Path.join(project.root, 'spec/spec.js'), newSpecs) 152 | 153 | copyFile = (from, to) -> 154 | ejs = fs.readFileSync(from) + "" 155 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, model : model })) 156 | sys.puts " * Created #{to}" 157 | 158 | copyFile "#{root}/templates/collection/collection.#{project.language()}", "app/collections/#{model.capitalize()}Collection.#{project.language()}" 159 | copyFile "#{root}/templates/collection/spec.#{project.language()}", "spec/collections/#{model.capitalize()}CollectionSpec.#{project.language()}" 160 | 161 | appendToSpecRunner "../spec/collections/#{model.capitalize()}CollectionSpec" 162 | 163 | task 'generate router', 'create a new router', (arguments) -> 164 | project = new Project(process.cwd()) 165 | 166 | if arguments[0] 167 | router = arguments[0].toLowerCase() 168 | else 169 | raise("Must supply a name for the router") 170 | 171 | appendToSpecRunner = (path) -> 172 | specs = fs.readFileSync(Path.join(project.root, 'spec/spec.js')) + "" 173 | marker = '// ' 174 | newSpecs = specs.split(marker) 175 | newSpecs.splice(1,0,",\n '#{path}'#{marker}") 176 | newSpecs = newSpecs.join('') 177 | 178 | fs.writeFileSync(Path.join(project.root, 'spec/spec.js'), newSpecs) 179 | 180 | copyFile = (from, to) -> 181 | ejs = fs.readFileSync(from) + "" 182 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, router : router })) 183 | sys.puts " * Created #{to}" 184 | 185 | try 186 | fs.mkdirSync "#{project.root}/app/views/#{router}", 0755 187 | fs.mkdirSync "#{project.root}/app/templates/#{router}", 0755 188 | catch e 189 | # ... 190 | 191 | copyFile "#{root}/templates/routers/router.#{project.language()}", "app/router/#{router.capitalize()}Router.#{project.language()}" 192 | copyFile "#{root}/templates/routers/spec.#{project.language()}", "spec/routers/#{router.capitalize}RouterSpec.#{project.language()}" 193 | 194 | appendToSpecRunner "../spec/routers/#{router.capitalize()}RouterSpec" 195 | 196 | task 'generate view', 'create a new view', (arguments) -> 197 | project = new Project(process.cwd()) 198 | 199 | if arguments[0] and arguments[1] 200 | router = arguments[0].toLowerCase() 201 | view = arguments[1].toLowerCase() 202 | else 203 | raise("Must supply a name for the router and then view") 204 | 205 | appendToSpecRunner = (path) -> 206 | specs = fs.readFileSync(Path.join(project.root, 'spec/spec.js')) + "" 207 | marker = '// ' 208 | newSpecs = specs.split(marker) 209 | newSpecs.splice(1,0,",\n '#{path}'#{marker}") 210 | newSpecs = newSpecs.join('') 211 | 212 | fs.writeFileSync(Path.join(project.root, 'spec/spec.js'), newSpecs) 213 | 214 | copyFile = (from, to) -> 215 | ejs = fs.readFileSync(from).toString() 216 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, router: router, view : view })) 217 | sys.puts " * Created #{to}" 218 | 219 | if !Path.existsSync("#{project.root}/app/views/#{router}") 220 | fs.mkdirSync "#{project.root}/app/views/#{router}", 0755 221 | 222 | if !Path.existsSync("#{project.root}/spec/views/#{router}") 223 | fs.mkdirSync "#{project.root}/spec/views/#{router}", 0755 224 | 225 | 226 | if !Path.existsSync("#{project.root}/app/templates/#{router}") 227 | fs.mkdirSync "#{project.root}/app/templates/#{router}", 0755 228 | 229 | copyFile "#{root}/templates/views/view.#{project.language()}", "app/views/#{router}/#{view.capitalize()}View.#{project.language()}" 230 | copyFile "#{root}/templates/templates/template.hbs", "app/templates/#{router}/#{view.capitalize()}Template.hbs" 231 | copyFile "#{root}/templates/views/spec.#{project.language()}", "spec/views/#{router}/#{view.capitalize()}ViewSpec.#{project.language()}" 232 | 233 | appendToSpecRunner "../spec/views/#{router}/#{view.capitalize()}ViewSpec" 234 | 235 | if !task.done 236 | sys.puts $usage 237 | process.exit() 238 | -------------------------------------------------------------------------------- /templates/lib/hbs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license handlebars hbs 0.2.1 - Alex Sexton, but Handlebars has it's own licensing junk 3 | * 4 | * Available via the MIT or new BSD license. 5 | * see: http://github.com/jrburke/require-cs for details on the plugin this was based off of 6 | */ 7 | 8 | /* Yes, deliciously evil. */ 9 | /*jslint evil: true, strict: false, plusplus: false, regexp: false */ 10 | /*global require: false, XMLHttpRequest: false, ActiveXObject: false, 11 | define: false, process: false, window: false */ 12 | define([ 13 | //>>excludeStart('excludeAfterBuild', pragmas.excludeAfterBuild) 14 | '../lib/Handlebars/i18nprecompile', 'Handlebars', 'underscore' 15 | // '../lib/Handlebars', '../lib/underscore', '../lib/Handlebars/i18nprecompile', '../lib/json2' 16 | //>>excludeEnd('excludeAfterBuild') 17 | ], function ( 18 | //>>excludeStart('excludeAfterBuild', pragmas.excludeAfterBuild) 19 | precompile, Handlebars, _ 20 | // Handlebars, _, precompile, JSON 21 | //>>excludeEnd('excludeAfterBuild') 22 | ) { 23 | // NOTE :: if you want to load template in production outside of the build, either precompile 24 | // them into modules or take out the conditional build stuff here 25 | 26 | //>>excludeStart('excludeAfterBuild', pragmas.excludeAfterBuild) 27 | var fs, getXhr, 28 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 29 | fetchText = function () { 30 | throw new Error('Environment unsupported.'); 31 | }, 32 | buildMap = [], 33 | filecode = "w+", 34 | templateExtension = ".hbs", 35 | customNameExtension = "@hbs", 36 | devStyleDirectory = "/demo/styles/", 37 | buildStyleDirectory = "/demo-build/styles/", 38 | buildCSSFileName = "screen.build.css"; 39 | 40 | if (typeof window !== "undefined" && window.navigator && window.document) { 41 | // Browser action 42 | getXhr = function () { 43 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 44 | var xhr, i, progId; 45 | if (typeof XMLHttpRequest !== "undefined") { 46 | return new XMLHttpRequest(); 47 | } else { 48 | for (i = 0; i < 3; i++) { 49 | progId = progIds[i]; 50 | try { 51 | xhr = new ActiveXObject(progId); 52 | } catch (e) {} 53 | 54 | if (xhr) { 55 | progIds = [progId]; // so faster next time 56 | break; 57 | } 58 | } 59 | } 60 | 61 | if (!xhr) { 62 | throw new Error("getXhr(): XMLHttpRequest not available"); 63 | } 64 | 65 | return xhr; 66 | }; 67 | 68 | fetchText = function (url, callback) { 69 | var xhr = getXhr(); 70 | xhr.open('GET', url, true); 71 | xhr.onreadystatechange = function (evt) { 72 | //Do not explicitly handle errors, those should be 73 | //visible via console output in the browser. 74 | if (xhr.readyState === 4) { 75 | callback(xhr.responseText); 76 | } 77 | }; 78 | xhr.send(null); 79 | }; 80 | 81 | } else if (typeof process !== "undefined" && 82 | process.versions && 83 | !!process.versions.node) { 84 | //Using special require.nodeRequire, something added by r.js. 85 | fs = require.nodeRequire('fs'); 86 | fetchText = function (path, callback) { 87 | callback(fs.readFileSync(path, 'utf8')); 88 | }; 89 | } else if (typeof java !== "undefined" && typeof java.io !== "undefined") { 90 | fetchText = function(path, callback) { 91 | var f = new java.io.File(path); 92 | var is = new java.io.FileReader(f); 93 | var reader = new java.io.BufferedReader(is); 94 | var line; 95 | var text = ""; 96 | while ((line = reader.readLine()) !== null) { 97 | text += new String(line) + "\n"; 98 | } 99 | reader.close(); 100 | callback(text); 101 | }; 102 | } 103 | 104 | var cache = {}; 105 | var fetchOrGetCached = function ( path, callback ){ 106 | if ( cache[path] ){ 107 | callback(cache[path]); 108 | } 109 | else { 110 | fetchText(path, function(data){ 111 | cache[path] = data; 112 | callback.call(this, data); 113 | }); 114 | } 115 | }; 116 | var styleList = [], styleMap = {}; 117 | //>>excludeEnd('excludeAfterBuild') 118 | 119 | return { 120 | 121 | setExtension : function (ext) { 122 | templateExtension = ext; 123 | }, 124 | 125 | get: function () { 126 | return Handlebars; 127 | }, 128 | 129 | write: function (pluginName, name, write) { 130 | 131 | if ( (name + customNameExtension ) in buildMap) { 132 | var text = buildMap[name + customNameExtension]; 133 | write.asModule(pluginName + "!" + name, text); 134 | } 135 | }, 136 | 137 | version: '1.0.3beta', 138 | 139 | load: function (name, parentRequire, load, config) { 140 | //>>excludeStart('excludeAfterBuild', pragmas.excludeAfterBuild) 141 | 142 | 143 | var compiledName = name + customNameExtension, 144 | partialDeps = []; 145 | 146 | function recursiveNodeSearch( statements, res ) { 147 | _(statements).forEach(function ( statement ) { 148 | if ( statement && statement.type && statement.type === 'partial' ) { 149 | res.push(statement.id.string); 150 | } 151 | if ( statement && statement.program && statement.program.statements ) { 152 | recursiveNodeSearch( statement.program.statements, res ); 153 | } 154 | }); 155 | return res; 156 | } 157 | 158 | // TODO :: use the parser to do this! 159 | function findPartialDeps( nodes ) { 160 | var res = []; 161 | if ( nodes && nodes.statements ) { 162 | res = recursiveNodeSearch( nodes.statements, [] ); 163 | } 164 | return _(res).unique(); 165 | } 166 | 167 | // See if the first item is a comment that's json 168 | function getMetaData( nodes ) { 169 | var statment, res, test; 170 | if ( nodes && nodes.statements ) { 171 | statement = nodes.statements[0]; 172 | if ( statement.type === "comment" ) { 173 | try { 174 | res = _.str.trim( statement.comment ); 175 | test = JSON.parse(res); 176 | return res; 177 | } 178 | catch (e) { 179 | return "{}"; 180 | } 181 | } 182 | } 183 | return "{}"; 184 | } 185 | function composeParts ( parts ) { 186 | if ( !parts ) { 187 | return []; 188 | } 189 | var res = [parts[0]], 190 | cur = parts[0], 191 | i; 192 | 193 | for (i = 1; i < parts.length; ++i) { 194 | if ( parts.hasOwnProperty(i) ) { 195 | cur += "." + parts[i]; 196 | res.push( cur ); 197 | } 198 | } 199 | return res; 200 | } 201 | 202 | function recursiveVarSearch( statements, res, prefix, helpersres ) { 203 | prefix = prefix ? prefix+"." : ""; 204 | 205 | var newprefix = "", flag = false; 206 | 207 | // loop through each statement 208 | _(statements).forEach(function ( statement ) { 209 | var parts, part, sideways; 210 | 211 | // if it's a mustache block 212 | if ( statement && statement.type && statement.type === 'mustache' ) { 213 | 214 | // If it has params, the first part is a helper or something 215 | if ( !statement.params || ! statement.params.length ) { 216 | parts = composeParts( statement.id.parts ); 217 | for( part in parts ) { 218 | if ( parts[ part ] ) { 219 | newprefix = parts[ part ] || newprefix; 220 | res.push( prefix + parts[ part ] ); 221 | } 222 | } 223 | res.push(prefix + statement.id.string); 224 | } 225 | 226 | // grab the params 227 | if ( statement.params ) { 228 | _(statement.params).forEach(function(param){ 229 | parts = composeParts( param.parts ); 230 | 231 | for(var part in parts ) { 232 | if ( parts[ part ] ) { 233 | newprefix = parts[part] || newprefix; 234 | helpersres.push(statement.id.string); 235 | res.push( prefix + parts[ part ] ); 236 | } 237 | } 238 | }); 239 | } 240 | } 241 | 242 | // If it's a meta block 243 | if ( statement && statement.mustache ) { 244 | recursiveVarSearch( [statement.mustache], res, prefix + newprefix, helpersres ); 245 | } 246 | 247 | // if it's a whole new program 248 | if ( statement && statement.program && statement.program.statements ) { 249 | sideways = recursiveVarSearch([statement.mustache],[], "", helpersres)[0] || ""; 250 | recursiveVarSearch( statement.program.statements, res, prefix + newprefix + (sideways ? (prefix+newprefix) ? "."+sideways : sideways : ""), helpersres); 251 | } 252 | }); 253 | return res; 254 | } 255 | 256 | // This finds the Helper dependencies since it's soooo similar 257 | function getExternalDeps( nodes ) { 258 | var res = []; 259 | var helpersres = []; 260 | 261 | if ( nodes && nodes.statements ) { 262 | res = recursiveVarSearch( nodes.statements, [], undefined, helpersres ); 263 | } 264 | 265 | var defaultHelpers = ["helperMissing", "blockHelperMissing", "each", "if", "unless", "with"]; 266 | 267 | return { 268 | vars : _(res).unique().map(function(e){ 269 | if ( e === "" ) { 270 | return '.'; 271 | } 272 | if ( e.length && e[e.length-1] === '.' ) { 273 | return e.substr(0,e.length-1) + '[]'; 274 | } 275 | return e; 276 | }), 277 | helpers : _(_(helpersres).unique().map(function(e){ 278 | if ( _(defaultHelpers).contains(e) ) { 279 | return undefined; 280 | } 281 | return e; 282 | })).compact() 283 | }; 284 | } 285 | 286 | var path = parentRequire.toUrl(name + templateExtension); 287 | fetchOrGetCached( parentRequire.toUrl('templates/i18n/'+(config.locale || "en_us")+'.json'), function (langMap) { 288 | // if local stuff isn't set, 404 isn't caught, and JSON.parse gets passed whatever generic 404 server text is sent back 289 | try { 290 | langMap = JSON.parse(langMap); 291 | } 292 | catch(e) { 293 | langMap = {}; 294 | } 295 | 296 | fetchText(path, function (text) { 297 | // for some reason it doesn't include hbs _first_ when i don't add it here... 298 | var nodes = Handlebars.parse(text), 299 | deps = findPartialDeps( nodes ), 300 | meta = getMetaData( nodes ), 301 | extDeps = getExternalDeps( nodes ), 302 | vars = extDeps.vars, 303 | helps = extDeps.helpers || [], 304 | depStr = deps.join("', 'hbs!").replace(/_/g, '/'), 305 | helpDepStr = helps.join("', 'templates/helpers/"), 306 | debugOutputStart = "", 307 | debugOutputEnd = "", 308 | debugProperties = "", 309 | metaObj, head, linkElem; 310 | 311 | if ( depStr ) { 312 | depStr = ",'hbs!" + depStr + "'"; 313 | } 314 | if ( helpDepStr ) { 315 | helpDepStr = ",'templates/helpers/" + helpDepStr + "'"; 316 | } 317 | // use a single helper file instead of trying to bring in individual ones. 318 | // TODO: make this configurable 319 | helpDepStr = ",'templates/helpers/all'"; 320 | if ( meta !== "{}" ) { 321 | try { 322 | metaObj = JSON.parse(meta); 323 | if ( metaObj && metaObj.styles ) { 324 | styleList = _.union(styleList, metaObj.styles); 325 | 326 | // In dev mode in the browser 327 | if ( require.isBrowser && ! config.isBuild ) { 328 | head = document.head || document.getElementsByTagName('head')[0]; 329 | _(metaObj.styles).forEach(function (style) { 330 | if ( !styleMap[style] ) { 331 | linkElem = document.createElement('link'); 332 | linkElem.href = config.baseUrl + 'styles/' + style + '.css'; 333 | linkElem.media = 'all'; 334 | linkElem.rel = 'stylesheet'; 335 | linkElem.type = 'text/css'; 336 | head.appendChild(linkElem); 337 | styleMap[style] = linkElem; 338 | } 339 | }); 340 | } 341 | else if ( config.isBuild ) { 342 | (function(){ 343 | var fs = require.nodeRequire('fs'), 344 | str = _(metaObj.styles).map(function (style) { 345 | if (!styleMap[style]) { 346 | styleMap[style] = true; 347 | return "@import url("+buildStyleDirectory+style+".css);\n"; 348 | } 349 | return ""; 350 | }).join("\n"); 351 | 352 | // I write out my import statements to a file in order to help me build stuff. 353 | // Then I use a tool to inline my import statements afterwards. (you can run r.js on it too) 354 | fs.open(__dirname + buildStyleDirectory + buildCSSFileName, filecode, '0666', function( e, id ) { 355 | fs.writeSync(id, str, null, encoding='utf8'); 356 | fs.close(id); 357 | }); 358 | filecode = "a"; 359 | })(); 360 | } 361 | } 362 | } 363 | catch(e){ 364 | console.log('error injecting styles'); 365 | } 366 | } 367 | 368 | if ( ! config.isBuild && ! config.serverRender ) { 369 | debugOutputStart = ""; 370 | debugOutputEnd = ""; 371 | debugProperties = "t.meta = " + meta + ";\n" + 372 | "t.helpers = " + JSON.stringify(helps) + ";\n" + 373 | "t.deps = " + JSON.stringify(deps) + ";\n" + 374 | "t.vars = " + JSON.stringify(vars) + ";\n"; 375 | } 376 | 377 | var prec = precompile( text, _.extend( langMap, config.localeMapping ) ); 378 | 379 | text = "/* START_TEMPLATE */\n" + 380 | "define(['hbs','Handlebars'"+depStr+helpDepStr+"], function( hbs, Handlebars ){ \n" + 381 | "var t = Handlebars.template(" + prec + ");\n" + 382 | "Handlebars.registerPartial('" + name.replace( /\//g , '_') + "', t);\n" + 383 | debugProperties + 384 | "return t;\n" + 385 | "});\n" + 386 | "/* END_TEMPLATE */\n"; 387 | 388 | //Hold on to the transformed text if a build. 389 | if (config.isBuild) { 390 | buildMap[compiledName] = text; 391 | } 392 | 393 | //IE with conditional comments on cannot handle the 394 | //sourceURL trick, so skip it if enabled. 395 | /*@if (@_jscript) @else @*/ 396 | if (!config.isBuild) { 397 | text += "\r\n//@ sourceURL=" + path; 398 | } 399 | /*@end@*/ 400 | 401 | for ( var i in deps ) { 402 | if ( deps.hasOwnProperty(i) ) { 403 | deps[ i ] = 'hbs!' + deps[ i ].replace(/_/g, '/'); 404 | } 405 | } 406 | 407 | if ( !config.isBuild ) { 408 | require( deps, function (){ 409 | load.fromText(compiledName, text); 410 | 411 | //Give result to load. Need to wait until the module 412 | //is fully parse, which will happen after this 413 | //execution. 414 | parentRequire([compiledName], function (value) { 415 | load(value); 416 | }); 417 | }); 418 | } 419 | else { 420 | load.fromText(compiledName, text); 421 | 422 | //Give result to load. Need to wait until the module 423 | //is fully parse, which will happen after this 424 | //execution. 425 | parentRequire([compiledName], function (value) { 426 | load(value); 427 | }); 428 | } 429 | }); 430 | }); 431 | //>>excludeEnd('excludeAfterBuild') 432 | } 433 | }; 434 | }); 435 | /* END_hbs_PLUGIN */ 436 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Tim Caswell 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | var sys = require('sys'); 27 | var fs = require('fs'); 28 | var path = require('path'); 29 | var http = require('http'); 30 | var url_parse = require("url").parse; 31 | 32 | // Used as a simple, convient 404 handler. 33 | function notFound(req, res, message) { 34 | message = (message || "Not Found\n") + ""; 35 | res.writeHead(404, { 36 | "Content-Type": "text/plain", 37 | "Content-Length": message.length 38 | }); 39 | if (req.method !== "HEAD") 40 | res.write(message); 41 | res.end(); 42 | } 43 | 44 | // Modifies req and res to call logger with a log line on each res.end 45 | // Think of it as "middleware" 46 | function logify(req, res, logger) { 47 | var end = res.end; 48 | res.end = function () { 49 | // Common Log Format (mostly) 50 | logger((req.socket && req.socket.remoteAddress) + " - - [" + (new Date()).toUTCString() + "]" 51 | + " \"" + req.method + " " + req.url 52 | + " HTTP/" + req.httpVersionMajor + "." + req.httpVersionMinor + "\" " 53 | + res.statusCode + " - \"" 54 | + (req.headers['referer'] || "") + "\" \"" + (req.headers["user-agent"] ? req.headers["user-agent"].split(' ')[0] : '') + "\""); 55 | return end.apply(this, arguments); 56 | } 57 | var writeHead = res.writeHead; 58 | res.writeHead = function (code) { 59 | res.statusCode = code; 60 | return writeHead.apply(this, arguments); 61 | } 62 | } 63 | 64 | exports.getServer = function getServer(logger) { 65 | 66 | logger = logger || sys.puts; 67 | 68 | var routes = []; 69 | 70 | // Adds a route the the current server 71 | function addRoute(method, pattern, handler, format) { 72 | if (typeof pattern === 'string') { 73 | pattern = new RegExp("^" + pattern + "$"); 74 | } 75 | var route = { 76 | method: method, 77 | pattern: pattern, 78 | handler: handler 79 | }; 80 | if (format !== undefined) { 81 | route.format = format; 82 | } 83 | routes.push(route); 84 | } 85 | 86 | // The four verbs are wrappers around addRoute 87 | function get(pattern, handler) { 88 | return addRoute("GET", pattern, handler); 89 | } 90 | function post(pattern, handler, format) { 91 | return addRoute("POST", pattern, handler, format); 92 | } 93 | function put(pattern, handler, format) { 94 | return addRoute("PUT", pattern, handler, format); 95 | } 96 | function del(pattern, handler) { 97 | return addRoute("DELETE", pattern, handler); 98 | } 99 | function head(pattern, handler) { 100 | return addRoute("HEAD", pattern, handler); 101 | } 102 | 103 | // This is a meta pattern that expands to a common RESTful mapping 104 | function resource(name, controller, format) { 105 | get(new RegExp('^/' + name + '$'), controller.index); 106 | get(new RegExp('^/' + name + '/([^/]+)$'), controller.show); 107 | post(new RegExp('^/' + name + '$'), controller.create, format); 108 | put(new RegExp('^/' + name + '/([^/]+)$'), controller.update, format); 109 | del(new RegExp('^/' + name + '/([^/]+)$'), controller.destroy); 110 | }; 111 | 112 | function resourceController(name, data, on_change) { 113 | data = data || []; 114 | on_change = on_change || function () {}; 115 | return { 116 | index: function (req, res) { 117 | res.simpleJson(200, {content: data, self: '/' + name}); 118 | }, 119 | show: function (req, res, id) { 120 | var item = data[id]; 121 | if (item) { 122 | res.simpleJson(200, {content: item, self: '/' + name + '/' + id}); 123 | } else { 124 | res.notFound(); 125 | } 126 | }, 127 | create: function (req, res) { 128 | req.jsonBody(function (json) { 129 | var item, id, url; 130 | item = json && json.content; 131 | if (!item) { 132 | res.notFound(); 133 | } else { 134 | data.push(item); 135 | id = data.length - 1; 136 | on_change(id); 137 | url = "/" + name + "/" + id; 138 | res.simpleJson(201, {content: item, self: url}, [["Location", url]]); 139 | } 140 | }); 141 | }, 142 | update: function (req, res, id) { 143 | req.jsonBody(function (json) { 144 | var item = json && json.content; 145 | if (!item) { 146 | res.notFound(); 147 | } else { 148 | data[id] = item; 149 | on_change(id); 150 | res.simpleJson(200, {content: item, self: "/" + name + "/" + id}); 151 | } 152 | }); 153 | }, 154 | destroy: function (req, res, id) { 155 | delete data[id]; 156 | on_change(id); 157 | res.simpleJson(200, "200 Destroyed"); 158 | } 159 | }; 160 | }; 161 | 162 | // Create the http server object 163 | var server = http.createServer(function (req, res) { 164 | 165 | // Enable logging on all requests using common-logger style 166 | logify(req, res, logger); 167 | 168 | var uri, path; 169 | 170 | // Performs an HTTP 302 redirect 171 | res.redirect = function redirect(location) { 172 | res.writeHead(302, {"Location": location}); 173 | res.end(); 174 | } 175 | 176 | // Performs an internal redirect 177 | res.innerRedirect = function innerRedirect(location) { 178 | logger("Internal Redirect: " + req.url + " -> " + location); 179 | req.url = location; 180 | doRoute(); 181 | } 182 | 183 | function simpleResponse(code, body, content_type, extra_headers) { 184 | res.writeHead(code, (extra_headers || []).concat( 185 | [ ["Content-Type", content_type], 186 | ["Content-Length", Buffer.byteLength(body, 'utf8')] 187 | ])); 188 | if (req.method !== "HEAD") 189 | res.write(body, 'utf8'); 190 | res.end(); 191 | } 192 | 193 | res.simpleText = function (code, body, extra_headers) { 194 | simpleResponse(code, body, "text/plain", extra_headers); 195 | }; 196 | 197 | res.simpleHtml = function (code, body, extra_headers) { 198 | simpleResponse(code, body, "text/html", extra_headers); 199 | }; 200 | 201 | res.simpleJson = function (code, json, extra_headers) { 202 | simpleResponse(code, JSON.stringify(json), "application/json", extra_headers); 203 | }; 204 | 205 | res.notFound = function (message) { 206 | notFound(req, res, message); 207 | }; 208 | 209 | res.onlyHead = function (code, extra_headers) { 210 | res.writeHead(code, (extra_headers || []).concat( 211 | [["Content-Type", content_type]])); 212 | res.end(); 213 | } 214 | 215 | function doRoute() { 216 | uri = url_parse(req.url); 217 | path = uri.pathname; 218 | 219 | for (var i = 0, l = routes.length; i < l; i += 1) { 220 | var route = routes[i]; 221 | if (req.method === route.method) { 222 | var match = path.match(route.pattern); 223 | if (match && match[0].length > 0) { 224 | match.shift(); 225 | match = match.map(function (part) { 226 | return part ? unescape(part) : part; 227 | }); 228 | match.unshift(res); 229 | match.unshift(req); 230 | if (route.format !== undefined) { 231 | var body = ""; 232 | req.setEncoding('utf8'); 233 | req.addListener('data', function (chunk) { 234 | body += chunk; 235 | }); 236 | req.addListener('end', function () { 237 | if (route.format === 'json') { 238 | try { 239 | body = JSON.parse(unescape(body)); 240 | } catch(e) { 241 | body = null; 242 | } 243 | } 244 | match.push(body); 245 | route.handler.apply(null, match); 246 | }); 247 | return; 248 | } 249 | var result = route.handler.apply(null, match); 250 | switch (typeof result) { 251 | case "string": 252 | res.simpleHtml(200, result); 253 | break; 254 | case "object": 255 | res.simpleJson(200, result); 256 | break; 257 | } 258 | 259 | return; 260 | } 261 | } 262 | } 263 | 264 | notFound(req, res); 265 | } 266 | doRoute(); 267 | 268 | }); 269 | 270 | 271 | function listen(port, host, callback) { 272 | port = port || 8080; 273 | 274 | if (typeof host === 'undefined' || host == '*') 275 | host = null; 276 | 277 | server.listen(port, host, callback); 278 | 279 | if (typeof port === 'number') { 280 | logger("node-router server instance at http://" + (host || '*') + ":" + port + "/"); 281 | } else { 282 | logger("node-router server instance at unix:" + port); 283 | } 284 | } 285 | 286 | function end() { 287 | return server.end(); 288 | } 289 | 290 | // Return a handle to the public facing functions from this closure as the 291 | // server object. 292 | return { 293 | get: get, 294 | post: post, 295 | put: put, 296 | del: del, 297 | resource: resource, 298 | resourceController: resourceController, 299 | listen: listen, 300 | end: end 301 | }; 302 | } 303 | 304 | 305 | 306 | 307 | exports.staticHandler = function (filename) { 308 | var body, headers; 309 | var content_type = mime.getMime(filename) 310 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 311 | 312 | function loadResponseData(req, res, callback) { 313 | if (body && headers) { 314 | callback(); 315 | return; 316 | } 317 | 318 | fs.readFile(filename, encoding, function (err, data) { 319 | if (err) { 320 | notFound(req, res, "Cannot find file: " + filename); 321 | return; 322 | } 323 | body = data; 324 | headers = [ [ "Content-Type" , content_type ], 325 | [ "Content-Length" , body.length ] 326 | ]; 327 | headers.push(["Cache-Control", "public"]); 328 | 329 | callback(); 330 | }); 331 | } 332 | 333 | return function (req, res) { 334 | loadResponseData(req, res, function () { 335 | res.writeHead(200, headers); 336 | if (req.method !== "HEAD") 337 | res.write(body, encoding); 338 | res.end(); 339 | }); 340 | }; 341 | }; 342 | 343 | exports.staticDirHandler = function(root, prefix) { 344 | function loadResponseData(req, res, filename, callback) { 345 | var content_type = mime.getMime(filename); 346 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 347 | 348 | fs.readFile(filename, encoding, function(err, data) { 349 | if(err) { 350 | notFound(req, res, "Cannot find file: " + filename); 351 | return; 352 | } 353 | var headers = [ [ "Content-Type" , content_type ], 354 | [ "Content-Length" , data.length ], 355 | [ "Cache-Control" , "public" ] 356 | ]; 357 | callback(headers, data, encoding); 358 | }); 359 | } 360 | 361 | return function (req, res) { 362 | // trim off any query/anchor stuff 363 | var filename = req.url.replace(/[\?|#].*$/, ''); 364 | if (prefix) filename = filename.replace(new RegExp('^'+prefix), ''); 365 | // make sure nobody can explore our local filesystem 366 | filename = path.join(root, filename.replace(/\.\.+/g, '.')); 367 | if (filename == root) filename = path.join(root, 'index.html'); 368 | loadResponseData(req, res, filename, function(headers, body, encoding) { 369 | res.writeHead(200, headers); 370 | if (req.method !== "HEAD") 371 | res.write(body, encoding); 372 | res.end(); 373 | }); 374 | }; 375 | }; 376 | 377 | 378 | // Mini mime module for static file serving 379 | var DEFAULT_MIME = 'application/octet-stream'; 380 | var mime = exports.mime = { 381 | 382 | getMime: function getMime(path) { 383 | var index = path.lastIndexOf("."); 384 | if (index < 0) { 385 | return DEFAULT_MIME; 386 | } 387 | return mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; 388 | }, 389 | 390 | TYPES : { ".3gp" : "video/3gpp", 391 | ".a" : "application/octet-stream", 392 | ".ai" : "application/postscript", 393 | ".aif" : "audio/x-aiff", 394 | ".aiff" : "audio/x-aiff", 395 | ".asc" : "application/pgp-signature", 396 | ".asf" : "video/x-ms-asf", 397 | ".asm" : "text/x-asm", 398 | ".asx" : "video/x-ms-asf", 399 | ".atom" : "application/atom+xml", 400 | ".au" : "audio/basic", 401 | ".avi" : "video/x-msvideo", 402 | ".bat" : "application/x-msdownload", 403 | ".bin" : "application/octet-stream", 404 | ".bmp" : "image/bmp", 405 | ".bz2" : "application/x-bzip2", 406 | ".c" : "text/x-c", 407 | ".cab" : "application/vnd.ms-cab-compressed", 408 | ".cc" : "text/x-c", 409 | ".chm" : "application/vnd.ms-htmlhelp", 410 | ".class" : "application/octet-stream", 411 | ".com" : "application/x-msdownload", 412 | ".conf" : "text/plain", 413 | ".cpp" : "text/x-c", 414 | ".crt" : "application/x-x509-ca-cert", 415 | ".css" : "text/css", 416 | ".csv" : "text/csv", 417 | ".cxx" : "text/x-c", 418 | ".deb" : "application/x-debian-package", 419 | ".der" : "application/x-x509-ca-cert", 420 | ".diff" : "text/x-diff", 421 | ".djv" : "image/vnd.djvu", 422 | ".djvu" : "image/vnd.djvu", 423 | ".dll" : "application/x-msdownload", 424 | ".dmg" : "application/octet-stream", 425 | ".doc" : "application/msword", 426 | ".dot" : "application/msword", 427 | ".dtd" : "application/xml-dtd", 428 | ".dvi" : "application/x-dvi", 429 | ".ear" : "application/java-archive", 430 | ".eml" : "message/rfc822", 431 | ".eps" : "application/postscript", 432 | ".exe" : "application/x-msdownload", 433 | ".f" : "text/x-fortran", 434 | ".f77" : "text/x-fortran", 435 | ".f90" : "text/x-fortran", 436 | ".flv" : "video/x-flv", 437 | ".for" : "text/x-fortran", 438 | ".gem" : "application/octet-stream", 439 | ".gemspec" : "text/x-script.ruby", 440 | ".gif" : "image/gif", 441 | ".gz" : "application/x-gzip", 442 | ".h" : "text/x-c", 443 | ".hh" : "text/x-c", 444 | ".htm" : "text/html", 445 | ".html" : "text/html", 446 | ".ico" : "image/vnd.microsoft.icon", 447 | ".ics" : "text/calendar", 448 | ".ifb" : "text/calendar", 449 | ".iso" : "application/octet-stream", 450 | ".jar" : "application/java-archive", 451 | ".java" : "text/x-java-source", 452 | ".jnlp" : "application/x-java-jnlp-file", 453 | ".jpeg" : "image/jpeg", 454 | ".jpg" : "image/jpeg", 455 | ".js" : "application/javascript", 456 | ".json" : "application/json", 457 | ".log" : "text/plain", 458 | ".m3u" : "audio/x-mpegurl", 459 | ".m4v" : "video/mp4", 460 | ".man" : "text/troff", 461 | ".mathml" : "application/mathml+xml", 462 | ".mbox" : "application/mbox", 463 | ".mdoc" : "text/troff", 464 | ".me" : "text/troff", 465 | ".mid" : "audio/midi", 466 | ".midi" : "audio/midi", 467 | ".mime" : "message/rfc822", 468 | ".mml" : "application/mathml+xml", 469 | ".mng" : "video/x-mng", 470 | ".mov" : "video/quicktime", 471 | ".mp3" : "audio/mpeg", 472 | ".mp4" : "video/mp4", 473 | ".mp4v" : "video/mp4", 474 | ".mpeg" : "video/mpeg", 475 | ".mpg" : "video/mpeg", 476 | ".ms" : "text/troff", 477 | ".msi" : "application/x-msdownload", 478 | ".odp" : "application/vnd.oasis.opendocument.presentation", 479 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet", 480 | ".odt" : "application/vnd.oasis.opendocument.text", 481 | ".ogg" : "application/ogg", 482 | ".p" : "text/x-pascal", 483 | ".pas" : "text/x-pascal", 484 | ".pbm" : "image/x-portable-bitmap", 485 | ".pdf" : "application/pdf", 486 | ".pem" : "application/x-x509-ca-cert", 487 | ".pgm" : "image/x-portable-graymap", 488 | ".pgp" : "application/pgp-encrypted", 489 | ".pkg" : "application/octet-stream", 490 | ".pl" : "text/x-script.perl", 491 | ".pm" : "text/x-script.perl-module", 492 | ".png" : "image/png", 493 | ".pnm" : "image/x-portable-anymap", 494 | ".ppm" : "image/x-portable-pixmap", 495 | ".pps" : "application/vnd.ms-powerpoint", 496 | ".ppt" : "application/vnd.ms-powerpoint", 497 | ".ps" : "application/postscript", 498 | ".psd" : "image/vnd.adobe.photoshop", 499 | ".py" : "text/x-script.python", 500 | ".qt" : "video/quicktime", 501 | ".ra" : "audio/x-pn-realaudio", 502 | ".rake" : "text/x-script.ruby", 503 | ".ram" : "audio/x-pn-realaudio", 504 | ".rar" : "application/x-rar-compressed", 505 | ".rb" : "text/x-script.ruby", 506 | ".rdf" : "application/rdf+xml", 507 | ".roff" : "text/troff", 508 | ".rpm" : "application/x-redhat-package-manager", 509 | ".rss" : "application/rss+xml", 510 | ".rtf" : "application/rtf", 511 | ".ru" : "text/x-script.ruby", 512 | ".s" : "text/x-asm", 513 | ".sgm" : "text/sgml", 514 | ".sgml" : "text/sgml", 515 | ".sh" : "application/x-sh", 516 | ".sig" : "application/pgp-signature", 517 | ".snd" : "audio/basic", 518 | ".so" : "application/octet-stream", 519 | ".svg" : "image/svg+xml", 520 | ".svgz" : "image/svg+xml", 521 | ".swf" : "application/x-shockwave-flash", 522 | ".t" : "text/troff", 523 | ".tar" : "application/x-tar", 524 | ".tbz" : "application/x-bzip-compressed-tar", 525 | ".tci" : "application/x-topcloud", 526 | ".tcl" : "application/x-tcl", 527 | ".tex" : "application/x-tex", 528 | ".texi" : "application/x-texinfo", 529 | ".texinfo" : "application/x-texinfo", 530 | ".text" : "text/plain", 531 | ".tif" : "image/tiff", 532 | ".tiff" : "image/tiff", 533 | ".torrent" : "application/x-bittorrent", 534 | ".tr" : "text/troff", 535 | ".ttf" : "application/x-font-ttf", 536 | ".txt" : "text/plain", 537 | ".vcf" : "text/x-vcard", 538 | ".vcs" : "text/x-vcalendar", 539 | ".vrml" : "model/vrml", 540 | ".war" : "application/java-archive", 541 | ".wav" : "audio/x-wav", 542 | ".wma" : "audio/x-ms-wma", 543 | ".wmv" : "video/x-ms-wmv", 544 | ".wmx" : "video/x-ms-wmx", 545 | ".wrl" : "model/vrml", 546 | ".wsdl" : "application/wsdl+xml", 547 | ".xbm" : "image/x-xbitmap", 548 | ".xhtml" : "application/xhtml+xml", 549 | ".xls" : "application/vnd.ms-excel", 550 | ".xml" : "application/xml", 551 | ".xpm" : "image/x-xpixmap", 552 | ".xsl" : "application/xml", 553 | ".xslt" : "application/xslt+xml", 554 | ".yaml" : "text/yaml", 555 | ".yml" : "text/yaml", 556 | ".zip" : "application/zip" 557 | } 558 | }; 559 | -------------------------------------------------------------------------------- /lib/parseopt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript Option Parser (parseopt) 3 | * Copyright (C) 2010 Mathias Panzenböck 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2.1 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 18 | */ 19 | 20 | /** 21 | * Construct a new OptionParser. 22 | * See the demo folder the end of this file for example usage. 23 | * 24 | * @param object params optional Parameter-Object 25 | * 26 | * ===== Parameter-Object ===== 27 | * { 28 | * minargs: integer, optional 29 | * maxargs: integer, optional 30 | * program: string, per default inferred from process.argv 31 | * strings: object, optional 32 | * Table of strings used in the output. See below. 33 | * options: array, optional 34 | * Array of option definitions. See below. 35 | * } 36 | * 37 | * ===== String-Table ===== 38 | * { 39 | * help: string, default: 'No help available for this option.' 40 | * usage: string, default: 'Usage' 41 | * options: string, default: 'OPTIONS' 42 | * arguments: string, default: 'ARGUMENTS' 43 | * required: string, default: 'required' 44 | * default: string, default: 'default' 45 | * base: string, default: 'base' 46 | * metavars: object, optional 47 | * Table of default metavar names per type. 48 | * Per default the type name in capital letters or derived 49 | * from the possible values. 50 | * } 51 | * 52 | * ===== Option Definition ===== 53 | * { 54 | * // Only when passed to the OptionParser constructor: 55 | * name: string or array 56 | * names: string or array, alias of name 57 | * Only one of both may be used at the same time. 58 | * 59 | * Names can be long options (e.g. '--foo') and short options 60 | * (e.g. '-f'). The first name is used to indentify the option. 61 | * Names musst be unique and may not contain '='. 62 | * 63 | * Short options may be combined when passed to a programm. E.g. 64 | * the options '-f' and '-b' can be combined to '-fb'. Only one 65 | * of these combined options may require an argument. 66 | * 67 | * Short options are separated from ther arguments by space, 68 | * long options per '='. If a long option requires an argument 69 | * and none is passed using '=' it also uses the next commandline 70 | * argument as it's argument (like short options). 71 | * 72 | * If '--' is encountered all remaining arguments are treated as 73 | * arguments and not as options. 74 | * 75 | * // General fields: 76 | * target: string, per deflault inferred from first name 77 | * This defines the name used in the returned options object. 78 | * Multiple options may have the same target. 79 | * default: any, default: undefined 80 | * The default value associated with a certain target and is 81 | * overwritten by each new option with the same target. 82 | * type: string, default: 'string', see below 83 | * required: boolean, default: false 84 | * redefinable: boolean, default: true 85 | * help: string, optional 86 | * details: array, optional 87 | * short list of details shown in braces after the option name 88 | * e.g. integer type options add 'base: '+base if base !== undefined 89 | * metavar: string or array, per deflault inferred from type 90 | * onOption: function (value) -> boolean, optional 91 | * Returning true canceles any further option parsing 92 | * and the parse() method returns null. 93 | * 94 | * // Type: string (alias: str) 95 | * // Type: boolean (alias: bool) 96 | * // Type: object (alias: obj) 97 | * 98 | * // Type: integer (alias: int) 99 | * min: integer, optional 100 | * max: integer, optional 101 | * NaN: boolean, default: false 102 | * base: integer, optional 103 | * 104 | * // Type: float (alias: number) 105 | * min: float, optional 106 | * max: float, optional 107 | * NaN: boolean, default: false 108 | * 109 | * // Type: flag 110 | * value: boolean, default: true 111 | * default: boolean, default: false 112 | * 113 | * // Type: option 114 | * value: any, per default inferred from first name 115 | * 116 | * // Type: enum 117 | * ignoreCase: boolean, default: true 118 | * values: array or object where the user enteres the field name of 119 | * the object and you get the value of the field 120 | * 121 | * // Type: record 122 | * create: function () -> object, default: Array 123 | * args: array of type definitions (type part of option definitions) 124 | * 125 | * // Type: custom 126 | * argc: integer, default: -1 127 | * Number of required arguments. 128 | * -1 means one optional argument. 129 | * parse: function (string, ...) -> value 130 | * stringify: function (value) -> string, optional 131 | * } 132 | * 133 | * ===== Option-Arguments ===== 134 | * For the following types exactly one argument is required: 135 | * integer, float, string, boolean, object, enum 136 | * 137 | * The following types have optional arguments: 138 | * flag 139 | * 140 | * The following types have no arguments: 141 | * option 142 | * 143 | * Custom types may set this through the argc field. 144 | */ 145 | function OptionParser (params) { 146 | this.optionsPerName = {}; 147 | this.defaultValues = {}; 148 | this.options = []; 149 | 150 | if (params !== undefined) { 151 | this.minargs = params.minargs == 0 ? undefined : params.minargs; 152 | this.maxargs = params.maxargs; 153 | this.program = params.program; 154 | this.strings = params.strings; 155 | 156 | if (this.minargs > this.maxargs) { 157 | throw new Error('minargs > maxargs'); 158 | } 159 | } 160 | 161 | if (this.strings === undefined) { 162 | this.strings = {}; 163 | } 164 | 165 | defaults(this.strings, { 166 | help: 'No help available for this option.', 167 | usage: 'Usage', 168 | options: 'OPTIONS', 169 | arguments: 'ARGUMENTS', 170 | required: 'required', 171 | default: 'default', 172 | base: 'base', 173 | metavars: {} 174 | }); 175 | 176 | defaults(this.strings.metavars, METAVARS); 177 | 178 | if (this.program === undefined) { 179 | this.program = process.argv[0] + ' ' + process.argv[1]; 180 | } 181 | 182 | if (params !== undefined && params.options !== undefined) { 183 | for (var i in params.options) { 184 | var opt = params.options[i]; 185 | var names; 186 | 187 | if (opt instanceof Array || typeof(opt) == 'string') { 188 | opt = undefined; 189 | names = opt; 190 | } 191 | else { 192 | names = opt.names; 193 | if (names === undefined) { 194 | names = opt.name; 195 | delete opt.name; 196 | } 197 | else { 198 | delete opt.names; 199 | } 200 | } 201 | this.add(names, opt); 202 | } 203 | } 204 | } 205 | 206 | OptionParser.prototype = { 207 | /** 208 | * Parse command line options. 209 | * 210 | * @param array args Commandline arguments. 211 | * If undefined process.argv.slice(2) is used. 212 | * 213 | * @return object 214 | * { 215 | * arguments: array 216 | * options: object, { target -> value } 217 | * } 218 | */ 219 | parse: function (args) { 220 | if (args === undefined) { 221 | args = process.argv.slice(2); 222 | } 223 | 224 | var data = { 225 | options: {}, 226 | arguments: [] 227 | }; 228 | 229 | for (var name in this.defaultValues) { 230 | var value = this.defaultValues[name]; 231 | 232 | if (value !== undefined) { 233 | data.options[this.optionsPerName[name].target] = value; 234 | } 235 | } 236 | 237 | var got = {}; 238 | var i = 0; 239 | for (; i < args.length; ++ i) { 240 | var arg = args[i]; 241 | 242 | if (arg == '--') { 243 | ++ i; 244 | break; 245 | } 246 | else if (/^--.+$/.test(arg)) { 247 | var j = arg.indexOf('='); 248 | var name, value = undefined; 249 | 250 | if (j == -1) { 251 | name = arg; 252 | } 253 | else { 254 | name = arg.substring(0,j); 255 | value = arg.substring(j+1); 256 | } 257 | 258 | var optdef = this.optionsPerName[name]; 259 | 260 | if (optdef === undefined) { 261 | throw new Error('unknown option: '+name); 262 | } 263 | 264 | if (value === undefined) { 265 | if (optdef.argc < 1) { 266 | value = optdef.value; 267 | } 268 | else if ((i + optdef.argc) >= args.length) { 269 | throw new Error('option '+name+' needs '+optdef.argc+' arguments'); 270 | } 271 | else { 272 | value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc)); 273 | i += optdef.argc; 274 | } 275 | } 276 | else if (optdef.argc == 0) { 277 | throw new Error('option '+name+' does not need an argument'); 278 | } 279 | else if (optdef.argc > 1) { 280 | throw new Error('option '+name+' needs '+optdef.argc+' arguments'); 281 | } 282 | else { 283 | value = optdef.parse(value); 284 | } 285 | 286 | if (!optdef.redefinable && optdef.target in got) { 287 | throw new Error('cannot redefine option '+name); 288 | } 289 | 290 | got[optdef.target] = true; 291 | data.options[optdef.target] = value; 292 | 293 | if (optdef.onOption && optdef.onOption(value) === true) { 294 | return null; 295 | } 296 | } 297 | else if (/^-.+$/.test(arg)) { 298 | if (arg.indexOf('=') != -1) { 299 | throw new Error('illegal option syntax: '+arg); 300 | } 301 | 302 | var tookarg = false; 303 | arg = arg.substring(1); 304 | 305 | for (var j = 0; j < arg.length; ++ j) { 306 | var name = '-'+arg[j]; 307 | var optdef = this.optionsPerName[name]; 308 | var value; 309 | 310 | if (optdef === undefined) { 311 | throw new Error('unknown option: '+name); 312 | } 313 | 314 | if (optdef.argc < 1) { 315 | value = optdef.value; 316 | } 317 | else { 318 | if (tookarg || (i+optdef.argc) >= args.length) { 319 | throw new Error('option '+name+' needs '+optdef.argc+' arguments'); 320 | } 321 | 322 | value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc)); 323 | i += optdef.argc; 324 | tookarg = true; 325 | } 326 | 327 | if (!optdef.redefinable && optdef.target in got) { 328 | throw new Error('redefined option: '+name); 329 | } 330 | 331 | got[optdef.target] = true; 332 | data.options[optdef.target] = value; 333 | 334 | if (optdef.onOption && optdef.onOption(value) === true) { 335 | return null; 336 | } 337 | } 338 | } 339 | else { 340 | data.arguments.push(arg); 341 | } 342 | } 343 | 344 | for (; i < args.length; ++ i) { 345 | data.arguments.push(args[i]); 346 | } 347 | 348 | var argc = data.arguments.length; 349 | if ((this.maxargs !== undefined && argc > this.maxargs) || 350 | (this.minargs !== undefined && argc < this.minargs)) { 351 | var msg = 'illegal number of arguments: ' + argc; 352 | 353 | if (this.minargs !== undefined) { 354 | msg += ', minumum is ' + this.minargs; 355 | if (this.maxargs !== undefined) { 356 | msg += ' and maximum is ' + this.maxargs; 357 | } 358 | } 359 | else { 360 | msg += ', maximum is ' + this.maxargs; 361 | } 362 | 363 | throw new Error(msg); 364 | } 365 | 366 | for (var i in this.options) { 367 | var optdef = this.options[i]; 368 | if (optdef.required && !(optdef.target in got)) { 369 | throw new Error('missing required option: ' + optdef.names[0]); 370 | } 371 | } 372 | 373 | return data; 374 | }, 375 | /** 376 | * Add an option definition. 377 | * 378 | * @param string or array names Option names 379 | * @param object optdef Option definition 380 | */ 381 | add: function (names, optdef) { 382 | if (typeof(names) == 'string') { 383 | names = [names]; 384 | } 385 | else if (names === undefined || names.length == 0) { 386 | throw new Error('no option name given'); 387 | } 388 | 389 | if (optdef === undefined) { 390 | optdef = {}; 391 | } 392 | 393 | optdef.names = names; 394 | 395 | for (var i in names) { 396 | var name = names[i]; 397 | var match = /(-*)(.*)/.exec(name); 398 | 399 | if (name.length == 0 || match[1].length < 1 || 400 | match[1].length > 2 || match[2].length == 0 || 401 | (match[1].length == 1 && match[2].length > 1) || 402 | match[2].indexOf('=') != -1) { 403 | throw new Error('illegal option name: ' + name); 404 | } 405 | 406 | if (name in this.optionsPerName) { 407 | throw new Error('option already exists: '+name); 408 | } 409 | } 410 | 411 | if (optdef.target === undefined) { 412 | var target = names[0].replace(/^--?/,''); 413 | 414 | if (target.toUpperCase() == target) { 415 | // FOO-BAR -> FOO_BAR 416 | target = target.replace(/[^a-zA-Z0-9]+/,'_'); 417 | } 418 | else { 419 | // foo-bar -> fooBar 420 | target = target.split(/[^a-zA-Z0-9]+/); 421 | for (var i = 1; i < target.length; ++ i) { 422 | var part = target[i]; 423 | 424 | if (part) { 425 | target[i] = part[0].toUpperCase() + part.substring(1); 426 | } 427 | } 428 | target = target.join(''); 429 | } 430 | 431 | optdef.target = target; 432 | } 433 | 434 | this._initType(optdef, optdef.names[0]); 435 | 436 | if (optdef.redefinable === undefined) { 437 | optdef.redefinable = true; 438 | } 439 | 440 | if (optdef.required === undefined) { 441 | optdef.required = false; 442 | } 443 | 444 | if (optdef.help === undefined) { 445 | optdef.help = this.strings.help; 446 | } 447 | else { 448 | optdef.help = optdef.help.trim(); 449 | } 450 | 451 | for (var i in names) { 452 | this.optionsPerName[names[i]] = optdef; 453 | } 454 | 455 | if (optdef.default !== undefined) { 456 | this.defaultValues[names[0]] = optdef.default; 457 | } 458 | 459 | this.options.push(optdef); 460 | }, 461 | /** 462 | * Show an error message, usage and exit program with exit code 1. 463 | * 464 | * @param string msg The error message 465 | * @param WriteStream out Where to write the message. 466 | * If undefined process.stdout is used. 467 | */ 468 | error: function (msg, out) { 469 | if (!out) { 470 | out = process.stdout; 471 | } 472 | out.write('*** '+msg+'\n\n'); 473 | this.usage(undefined, out); 474 | process.exit(1); 475 | }, 476 | /** 477 | * Print usage message. 478 | * 479 | * @param string help Optional additional help message. 480 | * @param WriteStream out Where to write the message. 481 | * If undefined process.stdout is used. 482 | */ 483 | usage: function (help, out) { 484 | if (!out) { 485 | out = process.stdout; 486 | } 487 | 488 | out.write(this.strings.usage+': '+this.program+' ['+ 489 | this.strings.options+']'+(this.maxargs != 0 ? 490 | ' ['+this.strings.arguments+']\n' : '\n')); 491 | out.write('\n'); 492 | out.write(this.strings.options+':\n'); 493 | 494 | for (var i in this.options) { 495 | var optdef = this.options[i]; 496 | var optnames = []; 497 | var metavar = optdef.metavar; 498 | 499 | if (metavar instanceof Array) { 500 | metavar = metavar.join(' '); 501 | } 502 | 503 | for (var j in optdef.names) { 504 | var optname = optdef.names[j]; 505 | 506 | if (metavar !== undefined) { 507 | if (optdef.argc < 2 && optname.substring(0,2) == '--') { 508 | if (optdef.argc < 0) { 509 | optname = optname+'[='+metavar+']'; 510 | } 511 | else { 512 | optname = optname+'='+metavar; 513 | } 514 | } 515 | else { 516 | optname = optname+' '+metavar; 517 | } 518 | } 519 | optnames.push(optname); 520 | } 521 | 522 | var details = optdef.details !== undefined ? optdef.details.slice() : []; 523 | if (optdef.required) { 524 | details.push(this.strings.required); 525 | } 526 | else if (optdef.argc > 0 && optdef.default !== undefined) { 527 | details.push(this.strings.default+': '+optdef.stringify(optdef.default)); 528 | } 529 | 530 | if (details.length > 0) { 531 | details = ' (' + details.join(', ') + ')'; 532 | } 533 | 534 | if (metavar !== undefined) { 535 | optnames[0] += details; 536 | out.write(' '+optnames.join('\n ')); 537 | } 538 | else { 539 | out.write(' '+optnames.join(', ')+details); 540 | } 541 | if (optdef.help) { 542 | var lines = optdef.help.split('\n'); 543 | for (var j in lines) { 544 | out.write('\n '+lines[j]); 545 | } 546 | } 547 | out.write('\n\n'); 548 | } 549 | 550 | if (help !== undefined) { 551 | out.write(help); 552 | if (help[help.length-1] != '\n') { 553 | out.write('\n'); 554 | } 555 | } 556 | }, 557 | _initType: function (optdef, name) { 558 | optdef.name = name; 559 | 560 | if (optdef.type === undefined) { 561 | optdef.type = 'string'; 562 | } 563 | else if (optdef.type in TYPE_ALIAS) { 564 | optdef.type = TYPE_ALIAS[optdef.type]; 565 | } 566 | 567 | switch (optdef.type) { 568 | case 'flag': 569 | if (optdef.value === undefined) { 570 | optdef.value = true; 571 | } 572 | optdef.parse = parseBool; 573 | optdef.argc = -1; 574 | 575 | if (optdef.default === undefined) { 576 | optdef.default = this.defaultValues[name]; 577 | 578 | if (optdef.default === undefined) { 579 | optdef.default = false; 580 | } 581 | } 582 | break; 583 | 584 | case 'option': 585 | optdef.argc = 0; 586 | 587 | if (optdef.value === undefined) { 588 | optdef.value = name.replace(/^--?/,''); 589 | } 590 | break; 591 | 592 | case 'enum': 593 | this._initEnum(optdef, name); 594 | break; 595 | 596 | case 'integer': 597 | case 'float': 598 | this._initNumber(optdef, name); 599 | break; 600 | 601 | case 'record': 602 | if (optdef.args === undefined || optdef.args.length == 0) { 603 | throw new Error('record '+name+' needs at least one argument'); 604 | } 605 | optdef.argc = 0; 606 | var metavar = []; 607 | for (var i in optdef.args) { 608 | var arg = optdef.args[i]; 609 | if (arg.target === undefined) { 610 | arg.target = i; 611 | } 612 | this._initType(arg, name+'['+i+']'); 613 | 614 | if (arg.argc < 1) { 615 | throw new Error('argument '+i+' of option '+name+ 616 | ' has illegal number of arguments'); 617 | } 618 | if (arg.metavar instanceof Array) { 619 | for (var j in arg.metavar) { 620 | metavar.push(arg.metavar[j]); 621 | } 622 | } 623 | else { 624 | metavar.push(arg.metavar); 625 | } 626 | delete arg.metavar; 627 | optdef.argc += arg.argc; 628 | } 629 | if (optdef.metavar === undefined) { 630 | optdef.metavar = metavar; 631 | } 632 | var onOption = optdef.onOption; 633 | if (onOption !== undefined) { 634 | optdef.onOption = function (values) { 635 | return onOption.apply(this, values); 636 | }; 637 | } 638 | if (optdef.create === undefined) { 639 | optdef.create = Array; 640 | } 641 | optdef.parse = function () { 642 | var values = this.create(); 643 | var parserIndex = 0; 644 | for (var i = 0; i < arguments.length;) { 645 | var arg = optdef.args[parserIndex ++]; 646 | var raw = []; 647 | for (var j = 0; j < arg.argc; ++ j) { 648 | raw.push(arguments[i+j]); 649 | } 650 | values[arg.target] = arg.parse.apply(arg, raw); 651 | i += arg.argc; 652 | } 653 | return values; 654 | }; 655 | break; 656 | 657 | case 'custom': 658 | if (optdef.argc === undefined || optdef.argc < -1) { 659 | optdef.argc = -1; 660 | } 661 | 662 | if (optdef.parse === undefined) { 663 | throw new Error( 664 | 'no parse function defined for custom type option '+name); 665 | } 666 | break; 667 | 668 | default: 669 | optdef.argc = 1; 670 | optdef.parse = PARSERS[optdef.type]; 671 | 672 | if (optdef.parse === undefined) { 673 | throw new Error('type of option '+name+' is unknown: '+optdef.type); 674 | } 675 | } 676 | 677 | initStringify(optdef); 678 | 679 | var count = 1; 680 | if (optdef.metavar === undefined) { 681 | optdef.metavar = this.strings.metavars[optdef.type]; 682 | } 683 | 684 | if (optdef.metavar === undefined) { 685 | count = 0; 686 | } 687 | else if (optdef.metavar instanceof Array) { 688 | count = optdef.metavar.length; 689 | } 690 | 691 | if (optdef.argc == -1) { 692 | if (count > 1) { 693 | throw new Error('illegal number of metavars for option '+name+ 694 | ': '+JSON.stringify(optdef.metavar)); 695 | } 696 | } 697 | else if (optdef.argc != count) { 698 | throw new Error('illegal number of metavars for option '+name+ 699 | ': '+JSON.stringify(optdef.metavar)); 700 | } 701 | }, 702 | _initEnum: function (optdef, name) { 703 | optdef.argc = 1; 704 | 705 | if (optdef.ignoreCase === undefined) { 706 | optdef.ignoreCase = true; 707 | } 708 | 709 | if (optdef.values === undefined || optdef.values.length == 0) { 710 | throw new Error('no values for enum '+name+' defined'); 711 | } 712 | 713 | initStringify(optdef); 714 | 715 | var labels = []; 716 | var values = {}; 717 | if (optdef.values instanceof Array) { 718 | for (var i in optdef.values) { 719 | var value = optdef.values[i]; 720 | var label = String(value); 721 | values[optdef.ignoreCase ? label.toLowerCase() : label] = value; 722 | labels.push(optdef.stringify(value)); 723 | } 724 | } 725 | else { 726 | for (var label in optdef.values) { 727 | var value = optdef.values[label]; 728 | values[optdef.ignoreCase ? label.toLowerCase() : label] = value; 729 | labels.push(optdef.stringify(label)); 730 | } 731 | labels.sort(); 732 | } 733 | optdef.values = values; 734 | 735 | 736 | if (optdef.metavar === undefined) { 737 | optdef.metavar = '<'+labels.join(', ')+'>'; 738 | } 739 | 740 | optdef.parse = function (s) { 741 | var value = values[optdef.ignoreCase ? s.toLowerCase() : s]; 742 | if (value !== undefined) { 743 | return value; 744 | } 745 | throw new Error('illegal value for option '+name+': '+s); 746 | }; 747 | }, 748 | _initNumber: function (optdef, name) { 749 | optdef.argc = 1; 750 | 751 | if (optdef.NaN === undefined) { 752 | optdef.NaN = false; 753 | } 754 | 755 | if (optdef.min > optdef.max) { 756 | throw new Error('min > max for option '+name); 757 | } 758 | 759 | var parse, toStr; 760 | if (optdef.type == 'integer') { 761 | parse = function (s) { 762 | var i = NaN; 763 | if (s.indexOf('.') == -1) { 764 | i = parseInt(s, optdef.base) 765 | } 766 | return i; 767 | }; 768 | if (optdef.base === undefined) { 769 | toStr = dec; 770 | } 771 | else { 772 | switch (optdef.base) { 773 | case 8: toStr = oct; break; 774 | case 10: toStr = dec; break; 775 | case 16: toStr = hex; break; 776 | default: toStr = function (val) { 777 | return val.toString(optdef.base); 778 | }; 779 | var detail = this.strings.base+': '+optdef.base; 780 | if (optdef.details) { 781 | optdef.details.push(detail); 782 | } 783 | else { 784 | optdef.details = [detail]; 785 | } 786 | } 787 | } 788 | } 789 | else { 790 | parse = parseFloat; 791 | toStr = dec; 792 | } 793 | 794 | if (optdef.metavar === undefined) { 795 | if (optdef.min === undefined && optdef.max === undefined) { 796 | optdef.metavar = this.strings.metavars[optdef.type]; 797 | } 798 | else if (optdef.min === undefined) { 799 | optdef.metavar = '...'+toStr(optdef.max); 800 | } 801 | else if (optdef.max === undefined) { 802 | optdef.metavar = toStr(optdef.min)+'...'; 803 | } 804 | else { 805 | optdef.metavar = toStr(optdef.min)+'...'+toStr(optdef.max); 806 | } 807 | } 808 | optdef.parse = function (s) { 809 | var n = parse(s); 810 | 811 | if ((!this.NaN && isNaN(n)) 812 | || (optdef.min !== undefined && n < optdef.min) 813 | || (optdef.max !== undefined && n > optdef.max)) { 814 | throw new Error('illegal value for option '+name+': '+s); 815 | } 816 | 817 | return n; 818 | }; 819 | } 820 | }; 821 | 822 | function initStringify (optdef) { 823 | if (optdef.stringify === undefined) { 824 | optdef.stringify = STRINGIFIERS[optdef.type]; 825 | } 826 | 827 | if (optdef.stringify === undefined) { 828 | optdef.stringify = stringifyAny; 829 | } 830 | } 831 | 832 | function defaults (target, defaults) { 833 | for (var name in defaults) { 834 | if (target[name] === undefined) { 835 | target[name] = defaults[name]; 836 | } 837 | } 838 | } 839 | 840 | function dec (val) { 841 | return val.toString(); 842 | } 843 | 844 | function oct (val) { 845 | return '0'+val.toString(8); 846 | } 847 | 848 | function hex (val) { 849 | return '0x'+val.toString(16); 850 | } 851 | 852 | const TRUE_VALUES = {true: true, on: true, 1: true, yes: true}; 853 | const FALSE_VALUES = {false: true, off: true, 0: true, no: true}; 854 | 855 | function parseBool (s) { 856 | s = s.trim().toLowerCase(); 857 | if (s in TRUE_VALUES) { 858 | return true; 859 | } 860 | else if (s in FALSE_VALUES) { 861 | return false; 862 | } 863 | else { 864 | throw new Error('illegal boolean value: '+s); 865 | } 866 | } 867 | 868 | function id (x) { 869 | return x; 870 | } 871 | 872 | const PARSERS = { 873 | boolean: parseBool, 874 | string: id, 875 | object: JSON.parse 876 | }; 877 | 878 | const TYPE_ALIAS = { 879 | int: 'integer', 880 | number: 'float', 881 | bool: 'boolean', 882 | str: 'string', 883 | obj: 'object' 884 | }; 885 | 886 | const METAVARS = { 887 | string: 'STRING', 888 | integer: 'INTEGER', 889 | float: 'FLOAT', 890 | boolean: 'BOOLEAN', 891 | object: 'OBJECT', 892 | enum: 'VALUE', 893 | custom: 'VALUE' 894 | }; 895 | 896 | function stringifyString(s) { 897 | if (/[\s'"\\<>,]/.test(s)) { 898 | // s = "'"+s.replace(/\\/g,'\\\\').replace(/'/g, "'\\''")+"'"; 899 | s = JSON.stringify(s); 900 | } 901 | return s; 902 | } 903 | 904 | function stringifyPrimitive(value) { 905 | return ''+value; 906 | } 907 | 908 | function stringifyAny (value) { 909 | if (value instanceof Array) { 910 | var buf = []; 911 | for (var i in value) { 912 | buf.push(stringifyAny(value[i])); 913 | } 914 | return buf.join(' '); 915 | } 916 | else if (typeof(value) == 'string') { 917 | return stringifyString(value); 918 | } 919 | else { 920 | return String(value); 921 | } 922 | } 923 | 924 | function stringifyInteger (value) { 925 | if (this.base === undefined) { 926 | return value.toString(); 927 | } 928 | 929 | switch (this.base) { 930 | case 8: return oct(value); 931 | case 16: return hex(value); 932 | default: return value.toString(this.base); 933 | } 934 | } 935 | 936 | function stringifyRecord (record) { 937 | var buf = []; 938 | for (var i = 0; i < this.args.length; ++ i) { 939 | var arg = this.args[i]; 940 | buf.push(arg.stringify(record[arg.target])); 941 | } 942 | return buf.join(' '); 943 | } 944 | 945 | const STRINGIFIERS = { 946 | string: stringifyString, 947 | integer: stringifyInteger, 948 | boolean: stringifyPrimitive, 949 | float: stringifyPrimitive, 950 | object: JSON.stringify, 951 | enum: stringifyAny, 952 | custom: stringifyAny, 953 | record: stringifyRecord 954 | }; 955 | 956 | exports.OptionParser = OptionParser; 957 | -------------------------------------------------------------------------------- /lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.1.3 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { return new wrapper(obj); }; 49 | 50 | // Export the Underscore object for **CommonJS**, with backwards-compatibility 51 | // for the old `require()` API. If we're not in CommonJS, add `_` to the 52 | // global object. 53 | if (typeof module !== 'undefined' && module.exports) { 54 | module.exports = _; 55 | _._ = _; 56 | } else { 57 | root._ = _; 58 | } 59 | 60 | // Current version. 61 | _.VERSION = '1.1.3'; 62 | 63 | // Collection Functions 64 | // -------------------- 65 | 66 | // The cornerstone, an `each` implementation, aka `forEach`. 67 | // Handles objects implementing `forEach`, arrays, and raw objects. 68 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 69 | var each = _.each = _.forEach = function(obj, iterator, context) { 70 | var value; 71 | if (nativeForEach && obj.forEach === nativeForEach) { 72 | obj.forEach(iterator, context); 73 | } else if (_.isNumber(obj.length)) { 74 | for (var i = 0, l = obj.length; i < l; i++) { 75 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 76 | } 77 | } else { 78 | for (var key in obj) { 79 | if (hasOwnProperty.call(obj, key)) { 80 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 81 | } 82 | } 83 | } 84 | }; 85 | 86 | // Return the results of applying the iterator to each element. 87 | // Delegates to **ECMAScript 5**'s native `map` if available. 88 | _.map = function(obj, iterator, context) { 89 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 90 | var results = []; 91 | each(obj, function(value, index, list) { 92 | results[results.length] = iterator.call(context, value, index, list); 93 | }); 94 | return results; 95 | }; 96 | 97 | // **Reduce** builds up a single result from a list of values, aka `inject`, 98 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 99 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 100 | var initial = memo !== void 0; 101 | if (nativeReduce && obj.reduce === nativeReduce) { 102 | if (context) iterator = _.bind(iterator, context); 103 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 104 | } 105 | each(obj, function(value, index, list) { 106 | if (!initial && index === 0) { 107 | memo = value; 108 | } else { 109 | memo = iterator.call(context, memo, value, index, list); 110 | } 111 | }); 112 | return memo; 113 | }; 114 | 115 | // The right-associative version of reduce, also known as `foldr`. 116 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 117 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 118 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 119 | if (context) iterator = _.bind(iterator, context); 120 | return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 121 | } 122 | var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); 123 | return _.reduce(reversed, iterator, memo, context); 124 | }; 125 | 126 | // Return the first value which passes a truth test. Aliased as `detect`. 127 | _.find = _.detect = function(obj, iterator, context) { 128 | var result; 129 | any(obj, function(value, index, list) { 130 | if (iterator.call(context, value, index, list)) { 131 | result = value; 132 | return true; 133 | } 134 | }); 135 | return result; 136 | }; 137 | 138 | // Return all the elements that pass a truth test. 139 | // Delegates to **ECMAScript 5**'s native `filter` if available. 140 | // Aliased as `select`. 141 | _.filter = _.select = function(obj, iterator, context) { 142 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 143 | var results = []; 144 | each(obj, function(value, index, list) { 145 | if (iterator.call(context, value, index, list)) results[results.length] = value; 146 | }); 147 | return results; 148 | }; 149 | 150 | // Return all the elements for which a truth test fails. 151 | _.reject = function(obj, iterator, context) { 152 | var results = []; 153 | each(obj, function(value, index, list) { 154 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 155 | }); 156 | return results; 157 | }; 158 | 159 | // Determine whether all of the elements match a truth test. 160 | // Delegates to **ECMAScript 5**'s native `every` if available. 161 | // Aliased as `all`. 162 | _.every = _.all = function(obj, iterator, context) { 163 | iterator = iterator || _.identity; 164 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 165 | var result = true; 166 | each(obj, function(value, index, list) { 167 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 168 | }); 169 | return result; 170 | }; 171 | 172 | // Determine if at least one element in the object matches a truth test. 173 | // Delegates to **ECMAScript 5**'s native `some` if available. 174 | // Aliased as `any`. 175 | var any = _.some = _.any = function(obj, iterator, context) { 176 | iterator = iterator || _.identity; 177 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 178 | var result = false; 179 | each(obj, function(value, index, list) { 180 | if (result = iterator.call(context, value, index, list)) return breaker; 181 | }); 182 | return result; 183 | }; 184 | 185 | // Determine if a given value is included in the array or object using `===`. 186 | // Aliased as `contains`. 187 | _.include = _.contains = function(obj, target) { 188 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 189 | var found = false; 190 | any(obj, function(value) { 191 | if (found = value === target) return true; 192 | }); 193 | return found; 194 | }; 195 | 196 | // Invoke a method (with arguments) on every item in a collection. 197 | _.invoke = function(obj, method) { 198 | var args = slice.call(arguments, 2); 199 | return _.map(obj, function(value) { 200 | return (method ? value[method] : value).apply(value, args); 201 | }); 202 | }; 203 | 204 | // Convenience version of a common use case of `map`: fetching a property. 205 | _.pluck = function(obj, key) { 206 | return _.map(obj, function(value){ return value[key]; }); 207 | }; 208 | 209 | // Return the maximum element or (element-based computation). 210 | _.max = function(obj, iterator, context) { 211 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); 212 | var result = {computed : -Infinity}; 213 | each(obj, function(value, index, list) { 214 | var computed = iterator ? iterator.call(context, value, index, list) : value; 215 | computed >= result.computed && (result = {value : value, computed : computed}); 216 | }); 217 | return result.value; 218 | }; 219 | 220 | // Return the minimum element (or element-based computation). 221 | _.min = function(obj, iterator, context) { 222 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); 223 | var result = {computed : Infinity}; 224 | each(obj, function(value, index, list) { 225 | var computed = iterator ? iterator.call(context, value, index, list) : value; 226 | computed < result.computed && (result = {value : value, computed : computed}); 227 | }); 228 | return result.value; 229 | }; 230 | 231 | // Sort the object's values by a criterion produced by an iterator. 232 | _.sortBy = function(obj, iterator, context) { 233 | return _.pluck(_.map(obj, function(value, index, list) { 234 | return { 235 | value : value, 236 | criteria : iterator.call(context, value, index, list) 237 | }; 238 | }).sort(function(left, right) { 239 | var a = left.criteria, b = right.criteria; 240 | return a < b ? -1 : a > b ? 1 : 0; 241 | }), 'value'); 242 | }; 243 | 244 | // Use a comparator function to figure out at what index an object should 245 | // be inserted so as to maintain order. Uses binary search. 246 | _.sortedIndex = function(array, obj, iterator) { 247 | iterator = iterator || _.identity; 248 | var low = 0, high = array.length; 249 | while (low < high) { 250 | var mid = (low + high) >> 1; 251 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 252 | } 253 | return low; 254 | }; 255 | 256 | // Safely convert anything iterable into a real, live array. 257 | _.toArray = function(iterable) { 258 | if (!iterable) return []; 259 | if (iterable.toArray) return iterable.toArray(); 260 | if (_.isArray(iterable)) return iterable; 261 | if (_.isArguments(iterable)) return slice.call(iterable); 262 | return _.values(iterable); 263 | }; 264 | 265 | // Return the number of elements in an object. 266 | _.size = function(obj) { 267 | return _.toArray(obj).length; 268 | }; 269 | 270 | // Array Functions 271 | // --------------- 272 | 273 | // Get the first element of an array. Passing **n** will return the first N 274 | // values in the array. Aliased as `head`. The **guard** check allows it to work 275 | // with `_.map`. 276 | _.first = _.head = function(array, n, guard) { 277 | return n && !guard ? slice.call(array, 0, n) : array[0]; 278 | }; 279 | 280 | // Returns everything but the first entry of the array. Aliased as `tail`. 281 | // Especially useful on the arguments object. Passing an **index** will return 282 | // the rest of the values in the array from that index onward. The **guard** 283 | // check allows it to work with `_.map`. 284 | _.rest = _.tail = function(array, index, guard) { 285 | return slice.call(array, _.isUndefined(index) || guard ? 1 : index); 286 | }; 287 | 288 | // Get the last element of an array. 289 | _.last = function(array) { 290 | return array[array.length - 1]; 291 | }; 292 | 293 | // Trim out all falsy values from an array. 294 | _.compact = function(array) { 295 | return _.filter(array, function(value){ return !!value; }); 296 | }; 297 | 298 | // Return a completely flattened version of an array. 299 | _.flatten = function(array) { 300 | return _.reduce(array, function(memo, value) { 301 | if (_.isArray(value)) return memo.concat(_.flatten(value)); 302 | memo[memo.length] = value; 303 | return memo; 304 | }, []); 305 | }; 306 | 307 | // Return a version of the array that does not contain the specified value(s). 308 | _.without = function(array) { 309 | var values = slice.call(arguments, 1); 310 | return _.filter(array, function(value){ return !_.include(values, value); }); 311 | }; 312 | 313 | // Produce a duplicate-free version of the array. If the array has already 314 | // been sorted, you have the option of using a faster algorithm. 315 | // Aliased as `unique`. 316 | _.uniq = _.unique = function(array, isSorted) { 317 | return _.reduce(array, function(memo, el, i) { 318 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el; 319 | return memo; 320 | }, []); 321 | }; 322 | 323 | // Produce an array that contains every item shared between all the 324 | // passed-in arrays. 325 | _.intersect = function(array) { 326 | var rest = slice.call(arguments, 1); 327 | return _.filter(_.uniq(array), function(item) { 328 | return _.every(rest, function(other) { 329 | return _.indexOf(other, item) >= 0; 330 | }); 331 | }); 332 | }; 333 | 334 | // Zip together multiple lists into a single array -- elements that share 335 | // an index go together. 336 | _.zip = function() { 337 | var args = slice.call(arguments); 338 | var length = _.max(_.pluck(args, 'length')); 339 | var results = new Array(length); 340 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 341 | return results; 342 | }; 343 | 344 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 345 | // we need this function. Return the position of the first occurrence of an 346 | // item in an array, or -1 if the item is not included in the array. 347 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 348 | _.indexOf = function(array, item) { 349 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 350 | for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; 351 | return -1; 352 | }; 353 | 354 | 355 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 356 | _.lastIndexOf = function(array, item) { 357 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 358 | var i = array.length; 359 | while (i--) if (array[i] === item) return i; 360 | return -1; 361 | }; 362 | 363 | // Generate an integer Array containing an arithmetic progression. A port of 364 | // the native Python `range()` function. See 365 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 366 | _.range = function(start, stop, step) { 367 | var args = slice.call(arguments), 368 | solo = args.length <= 1, 369 | start = solo ? 0 : args[0], 370 | stop = solo ? args[0] : args[1], 371 | step = args[2] || 1, 372 | len = Math.max(Math.ceil((stop - start) / step), 0), 373 | idx = 0, 374 | range = new Array(len); 375 | while (idx < len) { 376 | range[idx++] = start; 377 | start += step; 378 | } 379 | return range; 380 | }; 381 | 382 | // Function (ahem) Functions 383 | // ------------------ 384 | 385 | // Create a function bound to a given object (assigning `this`, and arguments, 386 | // optionally). Binding with arguments is also known as `curry`. 387 | _.bind = function(func, obj) { 388 | var args = slice.call(arguments, 2); 389 | return function() { 390 | return func.apply(obj || {}, args.concat(slice.call(arguments))); 391 | }; 392 | }; 393 | 394 | // Bind all of an object's methods to that object. Useful for ensuring that 395 | // all callbacks defined on an object belong to it. 396 | _.bindAll = function(obj) { 397 | var funcs = slice.call(arguments, 1); 398 | if (funcs.length == 0) funcs = _.functions(obj); 399 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 400 | return obj; 401 | }; 402 | 403 | // Memoize an expensive function by storing its results. 404 | _.memoize = function(func, hasher) { 405 | var memo = {}; 406 | hasher = hasher || _.identity; 407 | return function() { 408 | var key = hasher.apply(this, arguments); 409 | return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments)); 410 | }; 411 | }; 412 | 413 | // Delays a function for the given number of milliseconds, and then calls 414 | // it with the arguments supplied. 415 | _.delay = function(func, wait) { 416 | var args = slice.call(arguments, 2); 417 | return setTimeout(function(){ return func.apply(func, args); }, wait); 418 | }; 419 | 420 | // Defers a function, scheduling it to run after the current call stack has 421 | // cleared. 422 | _.defer = function(func) { 423 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 424 | }; 425 | 426 | // Internal function used to implement `_.throttle` and `_.debounce`. 427 | var limit = function(func, wait, debounce) { 428 | var timeout; 429 | return function() { 430 | var context = this, args = arguments; 431 | var throttler = function() { 432 | timeout = null; 433 | func.apply(context, args); 434 | }; 435 | if (debounce) clearTimeout(timeout); 436 | if (debounce || !timeout) timeout = setTimeout(throttler, wait); 437 | }; 438 | }; 439 | 440 | // Returns a function, that, when invoked, will only be triggered at most once 441 | // during a given window of time. 442 | _.throttle = function(func, wait) { 443 | return limit(func, wait, false); 444 | }; 445 | 446 | // Returns a function, that, as long as it continues to be invoked, will not 447 | // be triggered. The function will be called after it stops being called for 448 | // N milliseconds. 449 | _.debounce = function(func, wait) { 450 | return limit(func, wait, true); 451 | }; 452 | 453 | // Returns the first function passed as an argument to the second, 454 | // allowing you to adjust arguments, run code before and after, and 455 | // conditionally execute the original function. 456 | _.wrap = function(func, wrapper) { 457 | return function() { 458 | var args = [func].concat(slice.call(arguments)); 459 | return wrapper.apply(wrapper, args); 460 | }; 461 | }; 462 | 463 | // Returns a function that is the composition of a list of functions, each 464 | // consuming the return value of the function that follows. 465 | _.compose = function() { 466 | var funcs = slice.call(arguments); 467 | return function() { 468 | var args = slice.call(arguments); 469 | for (var i=funcs.length-1; i >= 0; i--) { 470 | args = [funcs[i].apply(this, args)]; 471 | } 472 | return args[0]; 473 | }; 474 | }; 475 | 476 | // Object Functions 477 | // ---------------- 478 | 479 | // Retrieve the names of an object's properties. 480 | // Delegates to **ECMAScript 5**'s native `Object.keys` 481 | _.keys = nativeKeys || function(obj) { 482 | if (_.isArray(obj)) return _.range(0, obj.length); 483 | var keys = []; 484 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; 485 | return keys; 486 | }; 487 | 488 | // Retrieve the values of an object's properties. 489 | _.values = function(obj) { 490 | return _.map(obj, _.identity); 491 | }; 492 | 493 | // Return a sorted list of the function names available on the object. 494 | // Aliased as `methods` 495 | _.functions = _.methods = function(obj) { 496 | return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort(); 497 | }; 498 | 499 | // Extend a given object with all the properties in passed-in object(s). 500 | _.extend = function(obj) { 501 | each(slice.call(arguments, 1), function(source) { 502 | for (var prop in source) obj[prop] = source[prop]; 503 | }); 504 | return obj; 505 | }; 506 | 507 | // Create a (shallow-cloned) duplicate of an object. 508 | _.clone = function(obj) { 509 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 510 | }; 511 | 512 | // Invokes interceptor with the obj, and then returns obj. 513 | // The primary purpose of this method is to "tap into" a method chain, in 514 | // order to perform operations on intermediate results within the chain. 515 | _.tap = function(obj, interceptor) { 516 | interceptor(obj); 517 | return obj; 518 | }; 519 | 520 | // Perform a deep comparison to check if two objects are equal. 521 | _.isEqual = function(a, b) { 522 | // Check object identity. 523 | if (a === b) return true; 524 | // Different types? 525 | var atype = typeof(a), btype = typeof(b); 526 | if (atype != btype) return false; 527 | // Basic equality test (watch out for coercions). 528 | if (a == b) return true; 529 | // One is falsy and the other truthy. 530 | if ((!a && b) || (a && !b)) return false; 531 | // One of them implements an isEqual()? 532 | if (a.isEqual) return a.isEqual(b); 533 | // Check dates' integer values. 534 | if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); 535 | // Both are NaN? 536 | if (_.isNaN(a) && _.isNaN(b)) return false; 537 | // Compare regular expressions. 538 | if (_.isRegExp(a) && _.isRegExp(b)) 539 | return a.source === b.source && 540 | a.global === b.global && 541 | a.ignoreCase === b.ignoreCase && 542 | a.multiline === b.multiline; 543 | // If a is not an object by this point, we can't handle it. 544 | if (atype !== 'object') return false; 545 | // Check for different array lengths before comparing contents. 546 | if (a.length && (a.length !== b.length)) return false; 547 | // Nothing else worked, deep compare the contents. 548 | var aKeys = _.keys(a), bKeys = _.keys(b); 549 | // Different object sizes? 550 | if (aKeys.length != bKeys.length) return false; 551 | // Recursive comparison of contents. 552 | for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; 553 | return true; 554 | }; 555 | 556 | // Is a given array or object empty? 557 | _.isEmpty = function(obj) { 558 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 559 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; 560 | return true; 561 | }; 562 | 563 | // Is a given value a DOM element? 564 | _.isElement = function(obj) { 565 | return !!(obj && obj.nodeType == 1); 566 | }; 567 | 568 | // Is a given value an array? 569 | // Delegates to ECMA5's native Array.isArray 570 | _.isArray = nativeIsArray || function(obj) { 571 | return !!(obj && obj.concat && obj.unshift && !obj.callee); 572 | }; 573 | 574 | // Is a given variable an arguments object? 575 | _.isArguments = function(obj) { 576 | return !!(obj && obj.callee); 577 | }; 578 | 579 | // Is a given value a function? 580 | _.isFunction = function(obj) { 581 | return !!(obj && obj.constructor && obj.call && obj.apply); 582 | }; 583 | 584 | // Is a given value a string? 585 | _.isString = function(obj) { 586 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); 587 | }; 588 | 589 | // Is a given value a number? 590 | _.isNumber = function(obj) { 591 | return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed)); 592 | }; 593 | 594 | // Is the given value NaN -- this one is interesting. NaN != NaN, and 595 | // isNaN(undefined) == true, so we make sure it's a number first. 596 | _.isNaN = function(obj) { 597 | return toString.call(obj) === '[object Number]' && isNaN(obj); 598 | }; 599 | 600 | // Is a given value a boolean? 601 | _.isBoolean = function(obj) { 602 | return obj === true || obj === false; 603 | }; 604 | 605 | // Is a given value a date? 606 | _.isDate = function(obj) { 607 | return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear); 608 | }; 609 | 610 | // Is the given value a regular expression? 611 | _.isRegExp = function(obj) { 612 | return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false)); 613 | }; 614 | 615 | // Is a given value equal to null? 616 | _.isNull = function(obj) { 617 | return obj === null; 618 | }; 619 | 620 | // Is a given variable undefined? 621 | _.isUndefined = function(obj) { 622 | return obj === void 0; 623 | }; 624 | 625 | // Utility Functions 626 | // ----------------- 627 | 628 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 629 | // previous owner. Returns a reference to the Underscore object. 630 | _.noConflict = function() { 631 | root._ = previousUnderscore; 632 | return this; 633 | }; 634 | 635 | // Keep the identity function around for default iterators. 636 | _.identity = function(value) { 637 | return value; 638 | }; 639 | 640 | // Run a function **n** times. 641 | _.times = function (n, iterator, context) { 642 | for (var i = 0; i < n; i++) iterator.call(context, i); 643 | }; 644 | 645 | // Add your own custom functions to the Underscore object, ensuring that 646 | // they're correctly added to the OOP wrapper as well. 647 | _.mixin = function(obj) { 648 | each(_.functions(obj), function(name){ 649 | addToWrapper(name, _[name] = obj[name]); 650 | }); 651 | }; 652 | 653 | // Generate a unique integer id (unique within the entire client session). 654 | // Useful for temporary DOM ids. 655 | var idCounter = 0; 656 | _.uniqueId = function(prefix) { 657 | var id = idCounter++; 658 | return prefix ? prefix + id : id; 659 | }; 660 | 661 | // By default, Underscore uses ERB-style template delimiters, change the 662 | // following template settings to use alternative delimiters. 663 | _.templateSettings = { 664 | evaluate : /<%([\s\S]+?)%>/g, 665 | interpolate : /<%=([\s\S]+?)%>/g 666 | }; 667 | 668 | // JavaScript micro-templating, similar to John Resig's implementation. 669 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 670 | // and correctly escapes quotes within interpolated code. 671 | _.template = function(str, data) { 672 | var c = _.templateSettings; 673 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + 674 | 'with(obj||{}){__p.push(\'' + 675 | str.replace(/\\/g, '\\\\') 676 | .replace(/'/g, "\\'") 677 | .replace(c.interpolate, function(match, code) { 678 | return "'," + code.replace(/\\'/g, "'") + ",'"; 679 | }) 680 | .replace(c.evaluate || null, function(match, code) { 681 | return "');" + code.replace(/\\'/g, "'") 682 | .replace(/[\r\n\t]/g, ' ') + "__p.push('"; 683 | }) 684 | .replace(/\r/g, '\\r') 685 | .replace(/\n/g, '\\n') 686 | .replace(/\t/g, '\\t') 687 | + "');}return __p.join('');"; 688 | var func = new Function('obj', tmpl); 689 | return data ? func(data) : func; 690 | }; 691 | 692 | // The OOP Wrapper 693 | // --------------- 694 | 695 | // If Underscore is called as a function, it returns a wrapped object that 696 | // can be used OO-style. This wrapper holds altered versions of all the 697 | // underscore functions. Wrapped objects may be chained. 698 | var wrapper = function(obj) { this._wrapped = obj; }; 699 | 700 | // Expose `wrapper.prototype` as `_.prototype` 701 | _.prototype = wrapper.prototype; 702 | 703 | // Helper function to continue chaining intermediate results. 704 | var result = function(obj, chain) { 705 | return chain ? _(obj).chain() : obj; 706 | }; 707 | 708 | // A method to easily add functions to the OOP wrapper. 709 | var addToWrapper = function(name, func) { 710 | wrapper.prototype[name] = function() { 711 | var args = slice.call(arguments); 712 | unshift.call(args, this._wrapped); 713 | return result(func.apply(_, args), this._chain); 714 | }; 715 | }; 716 | 717 | // Add all of the Underscore functions to the wrapper object. 718 | _.mixin(_); 719 | 720 | // Add all mutator Array functions to the wrapper. 721 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 722 | var method = ArrayProto[name]; 723 | wrapper.prototype[name] = function() { 724 | method.apply(this._wrapped, arguments); 725 | return result(this._wrapped, this._chain); 726 | }; 727 | }); 728 | 729 | // Add all accessor Array functions to the wrapper. 730 | each(['concat', 'join', 'slice'], function(name) { 731 | var method = ArrayProto[name]; 732 | wrapper.prototype[name] = function() { 733 | return result(method.apply(this._wrapped, arguments), this._chain); 734 | }; 735 | }); 736 | 737 | // Start chaining a wrapped Underscore object. 738 | wrapper.prototype.chain = function() { 739 | this._chain = true; 740 | return this; 741 | }; 742 | 743 | // Extracts the result from a wrapped and chained object. 744 | wrapper.prototype.value = function() { 745 | return this._wrapped; 746 | }; 747 | 748 | })(); -------------------------------------------------------------------------------- /templates/lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.1 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function() { 9 | 10 | // Baseline setup 11 | // -------------- 12 | // Establish the root object, `window` in the browser, or `global` on the server. 13 | var root = this; 14 | 15 | // Save the previous value of the `_` variable. 16 | var previousUnderscore = root._; 17 | 18 | // Establish the object that gets returned to break out of a loop iteration. 19 | var breaker = {}; 20 | 21 | // Save bytes in the minified (but not gzipped) version: 22 | var ArrayProto = Array.prototype, 23 | ObjProto = Object.prototype, 24 | FuncProto = Function.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys, 46 | nativeBind = FuncProto.bind; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { 50 | return new wrapper(obj); 51 | }; 52 | 53 | // Export the Underscore object for **Node.js** and **"CommonJS"**, with 54 | // backwards-compatibility for the old `require()` API. If we're not in 55 | // CommonJS, add `_` to the global object via a string identifier for 56 | // the Closure Compiler "advanced" mode, and optionally register as an 57 | // AMD module via define(). 58 | if (typeof exports !== 'undefined') { 59 | if (typeof module !== 'undefined' && module.exports) { 60 | exports = module.exports = _; 61 | } 62 | exports._ = _; 63 | } else { 64 | if (typeof define === 'function' && define.amd) { 65 | define('underscore', function() { 66 | return _; 67 | }); 68 | } 69 | root['_'] = _; 70 | } 71 | 72 | // Current version. 73 | _.VERSION = '1.3.1'; 74 | 75 | // Collection Functions 76 | // -------------------- 77 | // The cornerstone, an `each` implementation, aka `forEach`. 78 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 79 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 80 | var each = _.each = _.forEach = function(obj, iterator, context) { 81 | if (obj == null) return; 82 | if (nativeForEach && obj.forEach === nativeForEach) { 83 | obj.forEach(iterator, context); 84 | } else if (obj.length === +obj.length) { 85 | for (var i = 0, l = obj.length; i < l; i++) { 86 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 87 | } 88 | } else { 89 | for (var key in obj) { 90 | if (_.has(obj, key)) { 91 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 92 | } 93 | } 94 | } 95 | }; 96 | 97 | // Return the results of applying the iterator to each element. 98 | // Delegates to **ECMAScript 5**'s native `map` if available. 99 | _.map = _.collect = function(obj, iterator, context) { 100 | var results = []; 101 | if (obj == null) return results; 102 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 103 | each(obj, function(value, index, list) { 104 | results[results.length] = iterator.call(context, value, index, list); 105 | }); 106 | if (obj.length === +obj.length) results.length = obj.length; 107 | return results; 108 | }; 109 | 110 | // **Reduce** builds up a single result from a list of values, aka `inject`, 111 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 112 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 113 | var initial = arguments.length > 2; 114 | if (obj == null) obj = []; 115 | if (nativeReduce && obj.reduce === nativeReduce) { 116 | if (context) iterator = _.bind(iterator, context); 117 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 118 | } 119 | each(obj, function(value, index, list) { 120 | if (!initial) { 121 | memo = value; 122 | initial = true; 123 | } else { 124 | memo = iterator.call(context, memo, value, index, list); 125 | } 126 | }); 127 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 128 | return memo; 129 | }; 130 | 131 | // The right-associative version of reduce, also known as `foldr`. 132 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 133 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 134 | var initial = arguments.length > 2; 135 | if (obj == null) obj = []; 136 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 137 | if (context) iterator = _.bind(iterator, context); 138 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 139 | } 140 | var reversed = _.toArray(obj).reverse(); 141 | if (context && !initial) iterator = _.bind(iterator, context); 142 | return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); 143 | }; 144 | 145 | // Return the first value which passes a truth test. Aliased as `detect`. 146 | _.find = _.detect = function(obj, iterator, context) { 147 | var result; 148 | any(obj, function(value, index, list) { 149 | if (iterator.call(context, value, index, list)) { 150 | result = value; 151 | return true; 152 | } 153 | }); 154 | return result; 155 | }; 156 | 157 | // Return all the elements that pass a truth test. 158 | // Delegates to **ECMAScript 5**'s native `filter` if available. 159 | // Aliased as `select`. 160 | _.filter = _.select = function(obj, iterator, context) { 161 | var results = []; 162 | if (obj == null) return results; 163 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 164 | each(obj, function(value, index, list) { 165 | if (iterator.call(context, value, index, list)) results[results.length] = value; 166 | }); 167 | return results; 168 | }; 169 | 170 | // Return all the elements for which a truth test fails. 171 | _.reject = function(obj, iterator, context) { 172 | var results = []; 173 | if (obj == null) return results; 174 | each(obj, function(value, index, list) { 175 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 176 | }); 177 | return results; 178 | }; 179 | 180 | // Determine whether all of the elements match a truth test. 181 | // Delegates to **ECMAScript 5**'s native `every` if available. 182 | // Aliased as `all`. 183 | _.every = _.all = function(obj, iterator, context) { 184 | var result = true; 185 | if (obj == null) return result; 186 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 187 | each(obj, function(value, index, list) { 188 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 189 | }); 190 | return result; 191 | }; 192 | 193 | // Determine if at least one element in the object matches a truth test. 194 | // Delegates to **ECMAScript 5**'s native `some` if available. 195 | // Aliased as `any`. 196 | var any = _.some = _.any = function(obj, iterator, context) { 197 | iterator || (iterator = _.identity); 198 | var result = false; 199 | if (obj == null) return result; 200 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 201 | each(obj, function(value, index, list) { 202 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 203 | }); 204 | return !!result; 205 | }; 206 | 207 | // Determine if a given value is included in the array or object using `===`. 208 | // Aliased as `contains`. 209 | _.include = _.contains = function(obj, target) { 210 | var found = false; 211 | if (obj == null) return found; 212 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 213 | found = any(obj, function(value) { 214 | return value === target; 215 | }); 216 | return found; 217 | }; 218 | 219 | // Invoke a method (with arguments) on every item in a collection. 220 | _.invoke = function(obj, method) { 221 | var args = slice.call(arguments, 2); 222 | return _.map(obj, function(value) { 223 | return (_.isFunction(method) ? method || value : value[method]).apply(value, args); 224 | }); 225 | }; 226 | 227 | // Convenience version of a common use case of `map`: fetching a property. 228 | _.pluck = function(obj, key) { 229 | return _.map(obj, function(value) { 230 | return value[key]; 231 | }); 232 | }; 233 | 234 | // Return the maximum element or (element-based computation). 235 | _.max = function(obj, iterator, context) { 236 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); 237 | if (!iterator && _.isEmpty(obj)) return -Infinity; 238 | var result = { 239 | computed: -Infinity 240 | }; 241 | each(obj, function(value, index, list) { 242 | var computed = iterator ? iterator.call(context, value, index, list) : value; 243 | computed >= result.computed && (result = { 244 | value: value, 245 | computed: computed 246 | }); 247 | }); 248 | return result.value; 249 | }; 250 | 251 | // Return the minimum element (or element-based computation). 252 | _.min = function(obj, iterator, context) { 253 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); 254 | if (!iterator && _.isEmpty(obj)) return Infinity; 255 | var result = { 256 | computed: Infinity 257 | }; 258 | each(obj, function(value, index, list) { 259 | var computed = iterator ? iterator.call(context, value, index, list) : value; 260 | computed < result.computed && (result = { 261 | value: value, 262 | computed: computed 263 | }); 264 | }); 265 | return result.value; 266 | }; 267 | 268 | // Shuffle an array. 269 | _.shuffle = function(obj) { 270 | var shuffled = [], 271 | rand; 272 | each(obj, function(value, index, list) { 273 | if (index == 0) { 274 | shuffled[0] = value; 275 | } else { 276 | rand = Math.floor(Math.random() * (index + 1)); 277 | shuffled[index] = shuffled[rand]; 278 | shuffled[rand] = value; 279 | } 280 | }); 281 | return shuffled; 282 | }; 283 | 284 | // Sort the object's values by a criterion produced by an iterator. 285 | _.sortBy = function(obj, iterator, context) { 286 | return _.pluck(_.map(obj, function(value, index, list) { 287 | return { 288 | value: value, 289 | criteria: iterator.call(context, value, index, list) 290 | }; 291 | }).sort(function(left, right) { 292 | var a = left.criteria, 293 | b = right.criteria; 294 | return a < b ? -1 : a > b ? 1 : 0; 295 | }), 'value'); 296 | }; 297 | 298 | // Groups the object's values by a criterion. Pass either a string attribute 299 | // to group by, or a function that returns the criterion. 300 | _.groupBy = function(obj, val) { 301 | var result = {}; 302 | var iterator = _.isFunction(val) ? val : function(obj) { 303 | return obj[val]; 304 | }; 305 | each(obj, function(value, index) { 306 | var key = iterator(value, index); 307 | (result[key] || (result[key] = [])).push(value); 308 | }); 309 | return result; 310 | }; 311 | 312 | // Use a comparator function to figure out at what index an object should 313 | // be inserted so as to maintain order. Uses binary search. 314 | _.sortedIndex = function(array, obj, iterator) { 315 | iterator || (iterator = _.identity); 316 | var low = 0, 317 | high = array.length; 318 | while (low < high) { 319 | var mid = (low + high) >> 1; 320 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 321 | } 322 | return low; 323 | }; 324 | 325 | // Safely convert anything iterable into a real, live array. 326 | _.toArray = function(iterable) { 327 | if (!iterable) return []; 328 | if (iterable.toArray) return iterable.toArray(); 329 | if (_.isArray(iterable)) return slice.call(iterable); 330 | if (_.isArguments(iterable)) return slice.call(iterable); 331 | return _.values(iterable); 332 | }; 333 | 334 | // Return the number of elements in an object. 335 | _.size = function(obj) { 336 | return _.toArray(obj).length; 337 | }; 338 | 339 | // Array Functions 340 | // --------------- 341 | // Get the first element of an array. Passing **n** will return the first N 342 | // values in the array. Aliased as `head`. The **guard** check allows it to work 343 | // with `_.map`. 344 | _.first = _.head = function(array, n, guard) { 345 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 346 | }; 347 | 348 | // Returns everything but the last entry of the array. Especcialy useful on 349 | // the arguments object. Passing **n** will return all the values in 350 | // the array, excluding the last N. The **guard** check allows it to work with 351 | // `_.map`. 352 | _.initial = function(array, n, guard) { 353 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 354 | }; 355 | 356 | // Get the last element of an array. Passing **n** will return the last N 357 | // values in the array. The **guard** check allows it to work with `_.map`. 358 | _.last = function(array, n, guard) { 359 | if ((n != null) && !guard) { 360 | return slice.call(array, Math.max(array.length - n, 0)); 361 | } else { 362 | return array[array.length - 1]; 363 | } 364 | }; 365 | 366 | // Returns everything but the first entry of the array. Aliased as `tail`. 367 | // Especially useful on the arguments object. Passing an **index** will return 368 | // the rest of the values in the array from that index onward. The **guard** 369 | // check allows it to work with `_.map`. 370 | _.rest = _.tail = function(array, index, guard) { 371 | return slice.call(array, (index == null) || guard ? 1 : index); 372 | }; 373 | 374 | // Trim out all falsy values from an array. 375 | _.compact = function(array) { 376 | return _.filter(array, function(value) { 377 | return !!value; 378 | }); 379 | }; 380 | 381 | // Return a completely flattened version of an array. 382 | _.flatten = function(array, shallow) { 383 | return _.reduce(array, function(memo, value) { 384 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 385 | memo[memo.length] = value; 386 | return memo; 387 | }, []); 388 | }; 389 | 390 | // Return a version of the array that does not contain the specified value(s). 391 | _.without = function(array) { 392 | return _.difference(array, slice.call(arguments, 1)); 393 | }; 394 | 395 | // Produce a duplicate-free version of the array. If the array has already 396 | // been sorted, you have the option of using a faster algorithm. 397 | // Aliased as `unique`. 398 | _.uniq = _.unique = function(array, isSorted, iterator) { 399 | var initial = iterator ? _.map(array, iterator) : array; 400 | var result = []; 401 | _.reduce(initial, function(memo, el, i) { 402 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { 403 | memo[memo.length] = el; 404 | result[result.length] = array[i]; 405 | } 406 | return memo; 407 | }, []); 408 | return result; 409 | }; 410 | 411 | // Produce an array that contains the union: each distinct element from all of 412 | // the passed-in arrays. 413 | _.union = function() { 414 | return _.uniq(_.flatten(arguments, true)); 415 | }; 416 | 417 | // Produce an array that contains every item shared between all the 418 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 419 | _.intersection = _.intersect = function(array) { 420 | var rest = slice.call(arguments, 1); 421 | return _.filter(_.uniq(array), function(item) { 422 | return _.every(rest, function(other) { 423 | return _.indexOf(other, item) >= 0; 424 | }); 425 | }); 426 | }; 427 | 428 | // Take the difference between one array and a number of other arrays. 429 | // Only the elements present in just the first array will remain. 430 | _.difference = function(array) { 431 | var rest = _.flatten(slice.call(arguments, 1)); 432 | return _.filter(array, function(value) { 433 | return !_.include(rest, value); 434 | }); 435 | }; 436 | 437 | // Zip together multiple lists into a single array -- elements that share 438 | // an index go together. 439 | _.zip = function() { 440 | var args = slice.call(arguments); 441 | var length = _.max(_.pluck(args, 'length')); 442 | var results = new Array(length); 443 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 444 | return results; 445 | }; 446 | 447 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 448 | // we need this function. Return the position of the first occurrence of an 449 | // item in an array, or -1 if the item is not included in the array. 450 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 451 | // If the array is large and already in sort order, pass `true` 452 | // for **isSorted** to use binary search. 453 | _.indexOf = function(array, item, isSorted) { 454 | if (array == null) return -1; 455 | var i, l; 456 | if (isSorted) { 457 | i = _.sortedIndex(array, item); 458 | return array[i] === item ? i : -1; 459 | } 460 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 461 | for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; 462 | return -1; 463 | }; 464 | 465 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 466 | _.lastIndexOf = function(array, item) { 467 | if (array == null) return -1; 468 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 469 | var i = array.length; 470 | while (i--) if (i in array && array[i] === item) return i; 471 | return -1; 472 | }; 473 | 474 | // Generate an integer Array containing an arithmetic progression. A port of 475 | // the native Python `range()` function. See 476 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 477 | _.range = function(start, stop, step) { 478 | if (arguments.length <= 1) { 479 | stop = start || 0; 480 | start = 0; 481 | } 482 | step = arguments[2] || 1; 483 | 484 | var len = Math.max(Math.ceil((stop - start) / step), 0); 485 | var idx = 0; 486 | var range = new Array(len); 487 | 488 | while (idx < len) { 489 | range[idx++] = start; 490 | start += step; 491 | } 492 | 493 | return range; 494 | }; 495 | 496 | // Function (ahem) Functions 497 | // ------------------ 498 | // Reusable constructor function for prototype setting. 499 | var ctor = function() {}; 500 | 501 | // Create a function bound to a given object (assigning `this`, and arguments, 502 | // optionally). Binding with arguments is also known as `curry`. 503 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 504 | // We check for `func.bind` first, to fail fast when `func` is undefined. 505 | _.bind = function bind(func, context) { 506 | var bound, args; 507 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 508 | if (!_.isFunction(func)) throw new TypeError; 509 | args = slice.call(arguments, 2); 510 | return bound = function() { 511 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 512 | ctor.prototype = func.prototype; 513 | var self = new ctor; 514 | var result = func.apply(self, args.concat(slice.call(arguments))); 515 | if (Object(result) === result) return result; 516 | return self; 517 | }; 518 | }; 519 | 520 | // Bind all of an object's methods to that object. Useful for ensuring that 521 | // all callbacks defined on an object belong to it. 522 | _.bindAll = function(obj) { 523 | var funcs = slice.call(arguments, 1); 524 | if (funcs.length == 0) funcs = _.functions(obj); 525 | each(funcs, function(f) { 526 | obj[f] = _.bind(obj[f], obj); 527 | }); 528 | return obj; 529 | }; 530 | 531 | // Memoize an expensive function by storing its results. 532 | _.memoize = function(func, hasher) { 533 | var memo = {}; 534 | hasher || (hasher = _.identity); 535 | return function() { 536 | var key = hasher.apply(this, arguments); 537 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 538 | }; 539 | }; 540 | 541 | // Delays a function for the given number of milliseconds, and then calls 542 | // it with the arguments supplied. 543 | _.delay = function(func, wait) { 544 | var args = slice.call(arguments, 2); 545 | return setTimeout(function() { 546 | return func.apply(func, args); 547 | }, wait); 548 | }; 549 | 550 | // Defers a function, scheduling it to run after the current call stack has 551 | // cleared. 552 | _.defer = function(func) { 553 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 554 | }; 555 | 556 | // Returns a function, that, when invoked, will only be triggered at most once 557 | // during a given window of time. 558 | _.throttle = function(func, wait) { 559 | var context, args, timeout, throttling, more; 560 | var whenDone = _.debounce(function() { 561 | more = throttling = false; 562 | }, wait); 563 | return function() { 564 | context = this; 565 | args = arguments; 566 | var later = function() { 567 | timeout = null; 568 | if (more) func.apply(context, args); 569 | whenDone(); 570 | }; 571 | if (!timeout) timeout = setTimeout(later, wait); 572 | if (throttling) { 573 | more = true; 574 | } else { 575 | func.apply(context, args); 576 | } 577 | whenDone(); 578 | throttling = true; 579 | }; 580 | }; 581 | 582 | // Returns a function, that, as long as it continues to be invoked, will not 583 | // be triggered. The function will be called after it stops being called for 584 | // N milliseconds. 585 | _.debounce = function(func, wait) { 586 | var timeout; 587 | return function() { 588 | var context = this, 589 | args = arguments; 590 | var later = function() { 591 | timeout = null; 592 | func.apply(context, args); 593 | }; 594 | clearTimeout(timeout); 595 | timeout = setTimeout(later, wait); 596 | }; 597 | }; 598 | 599 | // Returns a function that will be executed at most one time, no matter how 600 | // often you call it. Useful for lazy initialization. 601 | _.once = function(func) { 602 | var ran = false, 603 | memo; 604 | return function() { 605 | if (ran) return memo; 606 | ran = true; 607 | return memo = func.apply(this, arguments); 608 | }; 609 | }; 610 | 611 | // Returns the first function passed as an argument to the second, 612 | // allowing you to adjust arguments, run code before and after, and 613 | // conditionally execute the original function. 614 | _.wrap = function(func, wrapper) { 615 | return function() { 616 | var args = [func].concat(slice.call(arguments, 0)); 617 | return wrapper.apply(this, args); 618 | }; 619 | }; 620 | 621 | // Returns a function that is the composition of a list of functions, each 622 | // consuming the return value of the function that follows. 623 | _.compose = function() { 624 | var funcs = arguments; 625 | return function() { 626 | var args = arguments; 627 | for (var i = funcs.length - 1; i >= 0; i--) { 628 | args = [funcs[i].apply(this, args)]; 629 | } 630 | return args[0]; 631 | }; 632 | }; 633 | 634 | // Returns a function that will only be executed after being called N times. 635 | _.after = function(times, func) { 636 | if (times <= 0) return func(); 637 | return function() { 638 | if (--times < 1) { 639 | return func.apply(this, arguments); 640 | } 641 | }; 642 | }; 643 | 644 | // Object Functions 645 | // ---------------- 646 | // Retrieve the names of an object's properties. 647 | // Delegates to **ECMAScript 5**'s native `Object.keys` 648 | _.keys = nativeKeys || 649 | function(obj) { 650 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 651 | var keys = []; 652 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 653 | return keys; 654 | }; 655 | 656 | // Retrieve the values of an object's properties. 657 | _.values = function(obj) { 658 | return _.map(obj, _.identity); 659 | }; 660 | 661 | // Return a sorted list of the function names available on the object. 662 | // Aliased as `methods` 663 | _.functions = _.methods = function(obj) { 664 | var names = []; 665 | for (var key in obj) { 666 | if (_.isFunction(obj[key])) names.push(key); 667 | } 668 | return names.sort(); 669 | }; 670 | 671 | // Extend a given object with all the properties in passed-in object(s). 672 | _.extend = function(obj) { 673 | each(slice.call(arguments, 1), function(source) { 674 | for (var prop in source) { 675 | obj[prop] = source[prop]; 676 | } 677 | }); 678 | return obj; 679 | }; 680 | 681 | // Fill in a given object with default properties. 682 | _.defaults = function(obj) { 683 | each(slice.call(arguments, 1), function(source) { 684 | for (var prop in source) { 685 | if (obj[prop] == null) obj[prop] = source[prop]; 686 | } 687 | }); 688 | return obj; 689 | }; 690 | 691 | // Create a (shallow-cloned) duplicate of an object. 692 | _.clone = function(obj) { 693 | if (!_.isObject(obj)) return obj; 694 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 695 | }; 696 | 697 | // Invokes interceptor with the obj, and then returns obj. 698 | // The primary purpose of this method is to "tap into" a method chain, in 699 | // order to perform operations on intermediate results within the chain. 700 | _.tap = function(obj, interceptor) { 701 | interceptor(obj); 702 | return obj; 703 | }; 704 | 705 | // Internal recursive comparison function. 706 | 707 | 708 | function eq(a, b, stack) { 709 | // Identical objects are equal. `0 === -0`, but they aren't identical. 710 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 711 | if (a === b) return a !== 0 || 1 / a == 1 / b; 712 | // A strict comparison is necessary because `null == undefined`. 713 | if (a == null || b == null) return a === b; 714 | // Unwrap any wrapped objects. 715 | if (a._chain) a = a._wrapped; 716 | if (b._chain) b = b._wrapped; 717 | // Invoke a custom `isEqual` method if one is provided. 718 | if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); 719 | if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); 720 | // Compare `[[Class]]` names. 721 | var className = toString.call(a); 722 | if (className != toString.call(b)) return false; 723 | switch (className) { 724 | // Strings, numbers, dates, and booleans are compared by value. 725 | case '[object String]': 726 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 727 | // equivalent to `new String("5")`. 728 | return a == String(b); 729 | case '[object Number]': 730 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 731 | // other numeric values. 732 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 733 | case '[object Date]': 734 | case '[object Boolean]': 735 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 736 | // millisecond representations. Note that invalid dates with millisecond representations 737 | // of `NaN` are not equivalent. 738 | return +a == +b; 739 | // RegExps are compared by their source patterns and flags. 740 | case '[object RegExp]': 741 | return a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; 742 | } 743 | if (typeof a != 'object' || typeof b != 'object') return false; 744 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 745 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 746 | var length = stack.length; 747 | while (length--) { 748 | // Linear search. Performance is inversely proportional to the number of 749 | // unique nested structures. 750 | if (stack[length] == a) return true; 751 | } 752 | // Add the first object to the stack of traversed objects. 753 | stack.push(a); 754 | var size = 0, 755 | result = true; 756 | // Recursively compare objects and arrays. 757 | if (className == '[object Array]') { 758 | // Compare array lengths to determine if a deep comparison is necessary. 759 | size = a.length; 760 | result = size == b.length; 761 | if (result) { 762 | // Deep compare the contents, ignoring non-numeric properties. 763 | while (size--) { 764 | // Ensure commutative equality for sparse arrays. 765 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 766 | } 767 | } 768 | } else { 769 | // Objects with different constructors are not equivalent. 770 | if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; 771 | // Deep compare objects. 772 | for (var key in a) { 773 | if (_.has(a, key)) { 774 | // Count the expected number of properties. 775 | size++; 776 | // Deep compare each member. 777 | if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; 778 | } 779 | } 780 | // Ensure that both objects contain the same number of properties. 781 | if (result) { 782 | for (key in b) { 783 | if (_.has(b, key) && !(size--)) break; 784 | } 785 | result = !size; 786 | } 787 | } 788 | // Remove the first object from the stack of traversed objects. 789 | stack.pop(); 790 | return result; 791 | } 792 | 793 | // Perform a deep comparison to check if two objects are equal. 794 | _.isEqual = function(a, b) { 795 | return eq(a, b, []); 796 | }; 797 | 798 | // Is a given array, string, or object empty? 799 | // An "empty" object has no enumerable own-properties. 800 | _.isEmpty = function(obj) { 801 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 802 | for (var key in obj) if (_.has(obj, key)) return false; 803 | return true; 804 | }; 805 | 806 | // Is a given value a DOM element? 807 | _.isElement = function(obj) { 808 | return !!(obj && obj.nodeType == 1); 809 | }; 810 | 811 | // Is a given value an array? 812 | // Delegates to ECMA5's native Array.isArray 813 | _.isArray = nativeIsArray || 814 | function(obj) { 815 | return toString.call(obj) == '[object Array]'; 816 | }; 817 | 818 | // Is a given variable an object? 819 | _.isObject = function(obj) { 820 | return obj === Object(obj); 821 | }; 822 | 823 | // Is a given variable an arguments object? 824 | _.isArguments = function(obj) { 825 | return toString.call(obj) == '[object Arguments]'; 826 | }; 827 | if (!_.isArguments(arguments)) { 828 | _.isArguments = function(obj) { 829 | return !!(obj && _.has(obj, 'callee')); 830 | }; 831 | } 832 | 833 | // Is a given value a function? 834 | _.isFunction = function(obj) { 835 | return toString.call(obj) == '[object Function]'; 836 | }; 837 | 838 | // Is a given value a string? 839 | _.isString = function(obj) { 840 | return toString.call(obj) == '[object String]'; 841 | }; 842 | 843 | // Is a given value a number? 844 | _.isNumber = function(obj) { 845 | return toString.call(obj) == '[object Number]'; 846 | }; 847 | 848 | // Is the given value `NaN`? 849 | _.isNaN = function(obj) { 850 | // `NaN` is the only value for which `===` is not reflexive. 851 | return obj !== obj; 852 | }; 853 | 854 | // Is a given value a boolean? 855 | _.isBoolean = function(obj) { 856 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 857 | }; 858 | 859 | // Is a given value a date? 860 | _.isDate = function(obj) { 861 | return toString.call(obj) == '[object Date]'; 862 | }; 863 | 864 | // Is the given value a regular expression? 865 | _.isRegExp = function(obj) { 866 | return toString.call(obj) == '[object RegExp]'; 867 | }; 868 | 869 | // Is a given value equal to null? 870 | _.isNull = function(obj) { 871 | return obj === null; 872 | }; 873 | 874 | // Is a given variable undefined? 875 | _.isUndefined = function(obj) { 876 | return obj === void 0; 877 | }; 878 | 879 | // Has own property? 880 | _.has = function(obj, key) { 881 | return hasOwnProperty.call(obj, key); 882 | }; 883 | 884 | // Utility Functions 885 | // ----------------- 886 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 887 | // previous owner. Returns a reference to the Underscore object. 888 | _.noConflict = function() { 889 | root._ = previousUnderscore; 890 | return this; 891 | }; 892 | 893 | // Keep the identity function around for default iterators. 894 | _.identity = function(value) { 895 | return value; 896 | }; 897 | 898 | // Run a function **n** times. 899 | _.times = function(n, iterator, context) { 900 | for (var i = 0; i < n; i++) iterator.call(context, i); 901 | }; 902 | 903 | // Escape a string for HTML interpolation. 904 | _.escape = function(string) { 905 | return ('' + string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); 906 | }; 907 | 908 | // Add your own custom functions to the Underscore object, ensuring that 909 | // they're correctly added to the OOP wrapper as well. 910 | _.mixin = function(obj) { 911 | each(_.functions(obj), function(name) { 912 | addToWrapper(name, _[name] = obj[name]); 913 | }); 914 | }; 915 | 916 | // Generate a unique integer id (unique within the entire client session). 917 | // Useful for temporary DOM ids. 918 | var idCounter = 0; 919 | _.uniqueId = function(prefix) { 920 | var id = idCounter++; 921 | return prefix ? prefix + id : id; 922 | }; 923 | 924 | // By default, Underscore uses ERB-style template delimiters, change the 925 | // following template settings to use alternative delimiters. 926 | _.templateSettings = { 927 | evaluate: /<%([\s\S]+?)%>/g, 928 | interpolate: /<%=([\s\S]+?)%>/g, 929 | escape: /<%-([\s\S]+?)%>/g 930 | }; 931 | 932 | // When customizing `templateSettings`, if you don't want to define an 933 | // interpolation, evaluation or escaping regex, we need one that is 934 | // guaranteed not to match. 935 | var noMatch = /.^/; 936 | 937 | // Within an interpolation, evaluation, or escaping, remove HTML escaping 938 | // that had been previously added. 939 | var unescape = function(code) { 940 | return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); 941 | }; 942 | 943 | // JavaScript micro-templating, similar to John Resig's implementation. 944 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 945 | // and correctly escapes quotes within interpolated code. 946 | _.template = function(str, data) { 947 | var c = _.templateSettings; 948 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + 'with(obj||{}){__p.push(\'' + str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(c.escape || noMatch, function(match, code) { 949 | return "',_.escape(" + unescape(code) + "),'"; 950 | }).replace(c.interpolate || noMatch, function(match, code) { 951 | return "'," + unescape(code) + ",'"; 952 | }).replace(c.evaluate || noMatch, function(match, code) { 953 | return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; 954 | }).replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t') + "');}return __p.join('');"; 955 | var func = new Function('obj', '_', tmpl); 956 | if (data) return func(data, _); 957 | return function(data) { 958 | return func.call(this, data, _); 959 | }; 960 | }; 961 | 962 | // Add a "chain" function, which will delegate to the wrapper. 963 | _.chain = function(obj) { 964 | return _(obj).chain(); 965 | }; 966 | 967 | // The OOP Wrapper 968 | // --------------- 969 | // If Underscore is called as a function, it returns a wrapped object that 970 | // can be used OO-style. This wrapper holds altered versions of all the 971 | // underscore functions. Wrapped objects may be chained. 972 | var wrapper = function(obj) { 973 | this._wrapped = obj; 974 | }; 975 | 976 | // Expose `wrapper.prototype` as `_.prototype` 977 | _.prototype = wrapper.prototype; 978 | 979 | // Helper function to continue chaining intermediate results. 980 | var result = function(obj, chain) { 981 | return chain ? _(obj).chain() : obj; 982 | }; 983 | 984 | // A method to easily add functions to the OOP wrapper. 985 | var addToWrapper = function(name, func) { 986 | wrapper.prototype[name] = function() { 987 | var args = slice.call(arguments); 988 | unshift.call(args, this._wrapped); 989 | return result(func.apply(_, args), this._chain); 990 | }; 991 | }; 992 | 993 | // Add all of the Underscore functions to the wrapper object. 994 | _.mixin(_); 995 | 996 | // Add all mutator Array functions to the wrapper. 997 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 998 | var method = ArrayProto[name]; 999 | wrapper.prototype[name] = function() { 1000 | var wrapped = this._wrapped; 1001 | method.apply(wrapped, arguments); 1002 | var length = wrapped.length; 1003 | if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; 1004 | return result(wrapped, this._chain); 1005 | }; 1006 | }); 1007 | 1008 | // Add all accessor Array functions to the wrapper. 1009 | each(['concat', 'join', 'slice'], function(name) { 1010 | var method = ArrayProto[name]; 1011 | wrapper.prototype[name] = function() { 1012 | return result(method.apply(this._wrapped, arguments), this._chain); 1013 | }; 1014 | }); 1015 | 1016 | // Start chaining a wrapped Underscore object. 1017 | wrapper.prototype.chain = function() { 1018 | this._chain = true; 1019 | return this; 1020 | }; 1021 | 1022 | // Extracts the result from a wrapped and chained object. 1023 | wrapper.prototype.value = function() { 1024 | return this._wrapped; 1025 | }; 1026 | 1027 | }).call(this); 1028 | --------------------------------------------------------------------------------