├── bin ├── capt ├── compiler.jar └── yuicompressor-2.4.2.jar ├── lib ├── parseopt.js ├── request.js ├── router.js ├── underscore.js └── yaml.js ├── package.json ├── readme.md ├── src ├── main.coffee └── project.coffee └── templates ├── collection ├── collection.coffee └── spec.coffee ├── config.yml ├── html ├── index.jst └── runner.jst ├── lib ├── backbone.js ├── jasmine-html.js ├── jasmine.css ├── jasmine.js ├── jquery.js ├── less.js └── underscore.js ├── models ├── model.coffee └── spec.coffee ├── routers ├── application.coffee ├── router.coffee └── spec.coffee ├── templates └── template.eco └── views ├── spec.coffee └── view.coffee /bin/capt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | root = __dirname + "/.." 4 | 5 | require("coffee-script") 6 | require(root + "/src/main.coffee") 7 | -------------------------------------------------------------------------------- /bin/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnolan/capt/94743374a1a93ca16ef62a4f21ea44d135e74d36/bin/compiler.jar -------------------------------------------------------------------------------- /bin/yuicompressor-2.4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnolan/capt/94743374a1a93ca16ef62a4f21ea44d135e74d36/bin/yuicompressor-2.4.2.jar -------------------------------------------------------------------------------- /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/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)}; -------------------------------------------------------------------------------- /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/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 | })(); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capt", 3 | "description": "Command line tool for creating backbone.js applications with coffeescript", 4 | "version": "0.5.6", 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 | -------------------------------------------------------------------------------- /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 | Requirements 6 | 7 | * Glob module 8 | 9 | Development build targets: 10 | 11 | * localhost using built in server 12 | * file:// with file watching and recompiling 13 | 14 | Production build targets: 15 | 16 | * web 17 | * html5 w/ offline manifest [planned] 18 | * nokia webruntime [planned] 19 | * phonegap [planned] 20 | * chrome appstore [planned] 21 | 22 | Optimizers supported: 23 | 24 | * Google Closure (css) 25 | * YUI (css and js) 26 | 27 | Languages supported: 28 | 29 | * Coffeescript 30 | * Javascript 31 | * LESS 32 | 33 | Testing framework: 34 | 35 | * Jasmine 36 | 37 | Libraries built in: 38 | 39 | * jQuery 40 | * backbone.js 41 | * underscore.js 42 | 43 | ## License 44 | 45 | [BSD Licensed](http://creativecommons.org/licenses/BSD/). YUI Compressor and Closure are licenced under their respective licences. 46 | 47 | ## Author 48 | 49 | Ben Nolan @bnolan bnolan@gmail.com 50 | 51 | 52 | # Changelog 53 | 54 | 0.5.4 - Fixed template which said `initailizer` instead of `initialize` -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | sys = require('sys') 3 | Path = require("path") 4 | Glob = require("glob").globSync 5 | 6 | root = __dirname + "/../" 7 | router = require("#{root}/lib/router") 8 | request = require("#{root}/lib/request") 9 | _ = require("#{root}/lib/underscore") 10 | yaml = require("#{root}/lib/yaml") 11 | server = router.getServer() 12 | Project = require("#{root}/src/project").Project 13 | 14 | String.prototype.capitalize = -> 15 | this.charAt(0).toUpperCase() + this.substring(1).toLowerCase() 16 | 17 | OptionParser = require("#{root}/lib/parseopt").OptionParser 18 | parser = new OptionParser { 19 | minargs : 0 20 | maxargs : 10 21 | } 22 | 23 | $usage = ''' 24 | Usage: 25 | 26 | capt new projectname 27 | - create a new project 28 | 29 | capt server 30 | - serve the current project on port 3000 31 | 32 | capt watch 33 | - watch the current project and recompile as needed 34 | 35 | Code generators: 36 | * capt generate model post 37 | * capt generate router posts 38 | * capt generate view posts show 39 | 40 | 41 | ''' 42 | 43 | # Parse command line 44 | data = parser.parse() 45 | 46 | # 47 | # Raise an error 48 | # 49 | raise = (error) -> 50 | sys.puts error 51 | process.exit() 52 | 53 | task = (command, description, func) -> 54 | length = command.split(' ').length 55 | 56 | if data.arguments.slice(0,length).join(" ") == command 57 | func(data.arguments.slice(length)) 58 | task.done = true 59 | 60 | # 61 | # Start the server 62 | # 63 | task 'server', 'start a webserver', (arguments) -> 64 | project = new Project(process.cwd()) 65 | 66 | server.get "/", (req, res, match) -> 67 | ejs = fs.readFileSync("#{project.root}/index.jst") + "" 68 | _.template(ejs, { project : project }) 69 | 70 | server.get "/spec/", (req, res) -> 71 | ejs = fs.readFileSync("#{project.root}/spec/index.jst") + "" 72 | _.template(ejs, { project : project }) 73 | 74 | server.get /(.*)/, router.staticDirHandler(project.root, '/') 75 | 76 | project.watchAndBuild() 77 | server.listen(3000) 78 | 79 | task 'build', 'concatenate and minify all javascript and stylesheets for production', (arguments) -> 80 | project = new Project(process.cwd()) 81 | 82 | sys.puts "Building #{project.name()}..." 83 | 84 | try 85 | fs.mkdirSync "#{project.root}/build", 0755 86 | catch e 87 | # .. ok .. 88 | 89 | output = "#{project.root}/build" 90 | 91 | try 92 | fs.mkdirSync output, 0755 93 | catch e 94 | # .. ok .. 95 | 96 | sys.puts " * Building css and js componenets" 97 | 98 | # Todo - emit an event from project when the build is complete 99 | project.build() 100 | 101 | setTimeout( => 102 | sys.puts " * #{output}/bundled-javascript.js" 103 | project.bundleJavascript("#{output}/bundled-javascript.js") 104 | project.bundleStylesheet("#{output}/bundled-stylesheet.css") 105 | 106 | # Recompile the index.html to use the bundled urls 107 | sys.puts " * #{output}/index.html" 108 | 109 | project.scriptIncludes = -> 110 | project.getScriptTagFor('./build/bundled-javascript.js') 111 | 112 | project.stylesheetIncludes = -> 113 | project.getStyleTagFor('./build/bundled-stylesheet.css') 114 | 115 | for file in project.getDependencies('static') 116 | project.compileFile(file) 117 | # ejs = fs.readFileSync("#{project.root}/index.jst") + "" 118 | # fs.writeFileSync("#{output}/index.html", _.template(ejs, { project : project })) 119 | 120 | , 2000) 121 | 122 | # sys.puts " * #{output}/bundled-stylesheet.css" 123 | # sys.puts " - " + project.getStylesheetDependencies().join("\n - ") 124 | # 125 | # project.bundleStylesheet("#{output}/bundled-stylesheet.css") 126 | # 127 | 128 | 129 | task 'watch', 'watch files and compile as needed', (arguments) -> 130 | project = new Project(process.cwd()) 131 | project.watchAndBuild() 132 | 133 | task 'new', 'create a new project', (arguments) -> 134 | project = arguments[0] or raise("Must supply a name for new project.") 135 | 136 | sys.puts " * Creating folders" 137 | 138 | dirs = ["", "spec", "spec/jasmine", "spec/models", "spec/routers", "spec/views", "app", "app/views", "app/templates", "app/routers", "app/models", "lib", "public", "public/stylesheets", "spec/fixtures"] 139 | 140 | for dir in dirs 141 | fs.mkdirSync "#{project}/#{dir}", 0755 142 | 143 | sys.puts " * Creating directory structure" 144 | 145 | libs = { 146 | "lib/jquery.js" : "lib/jquery.js", 147 | "lib/underscore.js" : "lib/underscore.js" 148 | "lib/backbone.js" : "lib/backbone.js" 149 | "lib/less.js" : "lib/less.js" 150 | "app/routers/application.coffee" : "routers/application.coffee" 151 | "spec/jasmine/jasmine-html.js" : "lib/jasmine-html.js" 152 | "spec/jasmine/jasmine.css" : "lib/jasmine.css" 153 | "spec/jasmine/jasmine.js" : "lib/jasmine.js" 154 | "config.yml" : "config.yml" 155 | "index.jst" : "html/index.jst" 156 | "spec/index.jst" : "html/runner.jst" 157 | } 158 | 159 | downloadLibrary = (path, lib) -> 160 | request { uri : lib }, (error, response, body) -> 161 | if (!error && response.statusCode == 200) 162 | sys.puts " * " + Path.basename(path) 163 | fs.writeFileSync("#{project}/#{path}", body) 164 | else 165 | sys.puts " * [ERROR] Could not download " + Path.basename(path) 166 | 167 | copyLibrary = (path, lib) -> 168 | fs.writeFileSync(Path.join(project, path), fs.readFileSync(lib) + "") 169 | 170 | for path, lib of libs 171 | if lib.match(/^http/) 172 | downloadLibrary(path, lib) 173 | else 174 | copyLibrary(path, Path.join(root, "templates/", lib)) 175 | 176 | task 'generate model', 'create a new model', (arguments) -> 177 | project = new Project(process.cwd()) 178 | 179 | if arguments[0] 180 | model = arguments[0].toLowerCase() 181 | else 182 | raise("Must supply a name for the model") 183 | 184 | copyFile = (from, to) -> 185 | ejs = fs.readFileSync(from) + "" 186 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, model : model })) 187 | sys.puts " * Created #{to}" 188 | 189 | copyFile "#{root}/templates/models/model.coffee", "app/models/#{model}.#{project.language()}" 190 | copyFile "#{root}/templates/models/spec.coffee", "spec/models/#{model}.#{project.language()}" 191 | 192 | 193 | task 'generate collection', 'create a new collection', (arguments) -> 194 | project = new Project(process.cwd()) 195 | 196 | if arguments[0] 197 | model = arguments[0].toLowerCase() 198 | else 199 | raise("Must supply a name for the model") 200 | 201 | copyFile = (from, to) -> 202 | ejs = fs.readFileSync(from) + "" 203 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, model : model })) 204 | sys.puts " * Created #{to}" 205 | 206 | copyFile "#{root}/templates/collection/collection.coffee", "app/models/#{model}_collection.#{project.language()}" 207 | copyFile "#{root}/templates/collection/spec.coffee", "spec/models/#{model}_collection.#{project.language()}" 208 | 209 | 210 | task 'generate router', 'create a new router', (arguments) -> 211 | project = new Project(process.cwd()) 212 | 213 | if arguments[0] 214 | router = arguments[0].toLowerCase() 215 | else 216 | raise("Must supply a name for the router") 217 | 218 | copyFile = (from, to) -> 219 | ejs = fs.readFileSync(from) + "" 220 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, router : router })) 221 | sys.puts " * Created #{to}" 222 | 223 | try 224 | fs.mkdirSync "#{project.root}/app/views/#{router}", 0755 225 | fs.mkdirSync "#{project.root}/spec/views/#{router}", 0755 226 | fs.mkdirSync "#{project.root}/app/templates/#{router}", 0755 227 | catch e 228 | # ... 229 | 230 | copyFile "#{root}/templates/routers/router.coffee", "app/routers/#{router}_router.#{project.language()}" 231 | copyFile "#{root}/templates/routers/spec.coffee", "spec/routers/#{router}_router.#{project.language()}" 232 | 233 | task 'generate view', 'create a new view', (arguments) -> 234 | project = new Project(process.cwd()) 235 | 236 | if arguments[0] and arguments[1] 237 | router = arguments[0].toLowerCase() 238 | view = arguments[1].toLowerCase() 239 | else 240 | raise("Must supply a name for the router and then view") 241 | 242 | copyFile = (from, to) -> 243 | ejs = fs.readFileSync(from).toString() 244 | fs.writeFileSync(Path.join(project.root, to), _.template(ejs, { project : project, router: router, view : view })) 245 | sys.puts " * Created #{to}" 246 | 247 | if !Path.existsSync("#{project.root}/app/views/#{router}") 248 | fs.mkdirSync "#{project.root}/app/views/#{router}", 0755 249 | 250 | if !Path.existsSync("#{project.root}/app/templates/#{router}") 251 | fs.mkdirSync "#{project.root}/app/templates/#{router}", 0755 252 | 253 | copyFile "#{root}/templates/views/view.coffee", "app/views/#{router}/#{view}.#{project.language()}" 254 | copyFile "#{root}/templates/templates/template.eco", "app/templates/#{router}/#{view}.eco" 255 | copyFile "#{root}/templates/views/spec.coffee", "spec/views/#{router}/#{view}.#{project.language()}" 256 | 257 | # task 'spec', 'run the specs', (arguments) -> 258 | # project = new Project(process.cwd()) 259 | # 260 | # sys.puts " * Running specs..." 261 | # 262 | # jasmine = require('jasmine-node') 263 | # 264 | # runLogger = (runner, log) -> 265 | # if runner.results().failedCount == 0 266 | # process.exit 0 267 | # else 268 | # process.exit 1 269 | # 270 | # jasmine.executeSpecsInFolder "spec/models", runLogger, true, true 271 | 272 | # No task was specified... 273 | 274 | if !task.done 275 | sys.puts $usage 276 | process.exit() 277 | -------------------------------------------------------------------------------- /src/project.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | sys = require('sys') 3 | yaml = require("#{root}/lib/yaml") 4 | Path = require("path") 5 | Glob = require("glob") 6 | exec = require('child_process').exec 7 | _ = require("#{root}/lib/underscore") 8 | CoffeeScript = require 'coffee-script' 9 | eco = require "eco" 10 | LessParser = require('less').Parser 11 | 12 | sys.puts "Capt:" 13 | sys.puts " * Using coffeescript version #{CoffeeScript.VERSION}" 14 | 15 | String.prototype.capitalize = -> 16 | this.charAt(0).toUpperCase() + this.substring(1).toLowerCase() 17 | 18 | class Project 19 | constructor: (cwd) -> 20 | @cwd = cwd 21 | @root = cwd 22 | try 23 | @yaml = yaml.eval(fs.readFileSync(@configPath()) + "\n\n") 24 | catch e 25 | sys.puts " * [ERROR] Unable to parse config.yml" 26 | sys.puts " * [ERROR] #{e.message}" 27 | process.exit(-1) 28 | 29 | # Include these sections of the config.yml (eg nokia, android or web) 30 | @targets = [] 31 | 32 | name : -> 33 | @cwd.replace(/.+\//,'') 34 | 35 | language : -> 36 | 'coffee' # or 'js' 37 | 38 | configPath : -> 39 | Path.join(@cwd, "config.yml") 40 | 41 | getScriptPathFor: (path) -> 42 | if Path.extname(path) == '.eco' 43 | # Eco templates 44 | Path.join(Path.dirname(path), ".js", Path.basename(path, '.eco') + '.js') 45 | else if Path.extname(path) == '.json' 46 | # Fixtures 47 | Path.join(Path.dirname(path), ".js", Path.basename(path, '.json') + '.js') 48 | else if Path.extname(path) == '.xml' 49 | # Fixtures 50 | Path.join(Path.dirname(path), ".js", Path.basename(path, '.xml') + '.js') 51 | else if Path.extname(path) == '.coffee' 52 | # Coffeescript 53 | Path.join(Path.dirname(path), ".js", Path.basename(path, '.coffee') + '.js') 54 | else 55 | # Javascript 56 | path 57 | 58 | getScriptTagFor: (path) -> 59 | path = @getScriptPathFor(path) 60 | "" 61 | 62 | getStyleTagFor: (path) -> 63 | if Path.extname(path) == '.less' 64 | # LESS CSS compiler 65 | csspath = Path.join(Path.dirname(path), "." + Path.basename(path, '.less') + '.css') 66 | "" 67 | else 68 | "" 69 | 70 | bundleStylesheet : (filename) -> 71 | index = 0 72 | 73 | inputs = for sheet in @getStylesheetDependencies() 74 | path = Path.join(@root, @getScriptPathFor(sheet)) 75 | 76 | inputs = inputs.join " " 77 | 78 | exec("cat #{inputs} > #{filename}") 79 | 80 | 81 | # createManifest : -> 82 | # [].concat( 83 | # @getDependencies('specs') 84 | # @getScriptDependencies() 85 | # @getStylesheetDependencies() 86 | # ) 87 | # 88 | bundleJavascript : (filename) -> 89 | index = 0 90 | 91 | inputs = for script in @getScriptDependencies() 92 | path = Path.join(@root, @getScriptPathFor(script)) 93 | 94 | inputs = inputs.join " " 95 | 96 | # sys.puts inputs 97 | # sys.puts "java -jar #{root}/bin/compiler.jar #{inputs} --js_output_file #{filename}" 98 | 99 | # exec("java -jar #{root}/bin/compiler.jar --compilation_level WHITESPACE_ONLY #{inputs} --js_output_file #{filename}") 100 | # sys.puts("cat #{inputs} > #{filename}") 101 | exec("cat #{inputs} > #{filename}") 102 | 103 | getScriptDependencies : () -> 104 | [].concat( 105 | @getDependencies('javascripts') 106 | @getDependencies('templates') 107 | ) 108 | # 109 | # 110 | # scripts = _([]) 111 | # 112 | # @getDependencies('templates') 113 | # 114 | # for pathspec in @yaml.javascripts 115 | # for path in Glob(Path.join(@cwd, pathspec)) 116 | # path = path.replace(@cwd, '').replace(/^[.\/]+/,'/') 117 | # scripts.push path 118 | # # 119 | # # for target in @targets 120 | # # for pathspec in @yaml[target] 121 | # # for path in Glob(Path.join(@cwd, pathspec)) 122 | # # path = path.replace(@cwd, '') 123 | # # scripts.push path 124 | # 125 | # scripts.unique() 126 | 127 | getDependencies: (section) -> 128 | if !@yaml[section] 129 | if section == 'static' 130 | return ['/index.jst', '/spec/index.jst'] 131 | 132 | sys.puts "[WARNING] Unable to find section '#{section}' in config.yml" 133 | return [] 134 | 135 | result = _([]) 136 | 137 | for pathspec in @yaml[section] 138 | for path in Glob(Path.join(@cwd, pathspec), { sync : true }) 139 | path = path.replace(@cwd, '').replace(/^[.\/]+/,'') 140 | result.push path 141 | 142 | result.value() 143 | 144 | getStylesheetDependencies : -> 145 | result = _([]) 146 | 147 | for pathspec in @yaml.stylesheets 148 | for path in Glob(Path.join(@cwd, pathspec), { sync : true }) 149 | path = path.replace(@cwd, '').replace(/^[.\/]+/,'') 150 | result.push path 151 | 152 | result.unique() 153 | 154 | stylesheetIncludes : -> 155 | tags = for css in @getStylesheetDependencies() 156 | @getStyleTagFor css 157 | 158 | tags.join("\n ") 159 | 160 | specIncludes : -> 161 | tags = for script in @getScriptDependencies() 162 | @getScriptTagFor(Path.join("..", script)) 163 | 164 | for script in @getDependencies('specs') 165 | tags.push @getScriptTagFor(Path.join("..", script)) 166 | 167 | # for script in @getDependencies('fixtures') 168 | # tags.push @getScriptTagFor(Path.join("..", script)) 169 | 170 | tags.join("\n ") 171 | 172 | scriptIncludes : -> 173 | tags = for script in @getScriptDependencies() 174 | @getScriptTagFor script 175 | 176 | tags.join("\n ") 177 | 178 | compileFile : (file) -> 179 | extension = Path.extname(file) 180 | 181 | if extension == ".coffee" 182 | @_compileCoffee(file) 183 | else if extension == ".less" 184 | @_compileLess(file) 185 | else if extension == ".xml" 186 | @_compileXml(file) 187 | else if extension == ".json" 188 | @_compileJson(file) 189 | else if extension == ".eco" 190 | @_compileEco(file) 191 | else if extension == ".jst" 192 | @_compileJst(file) 193 | else 194 | # do nothing... 195 | 196 | getWatchables : -> 197 | [].concat( 198 | @getDependencies('static') 199 | @getDependencies('specs') 200 | # @getDependencies('fixtures') 201 | @getScriptDependencies() 202 | @getStylesheetDependencies() 203 | ) 204 | 205 | _compileLess : (file) -> 206 | parser = new LessParser { 207 | # Specify search paths for @import directives 208 | paths: ['.', 'public/stylesheets'], 209 | 210 | # Specify a filename, for better error messages 211 | filename: file 212 | } 213 | 214 | fs.readFile Path.join(@root, file), (err, code) => 215 | throw err if err 216 | 217 | path = Path.dirname(file) 218 | outpath = Path.join(path, "." + Path.basename(file, ".less") + ".css") 219 | 220 | try 221 | fs.mkdirSync Path.join(@root, path), 0755 222 | catch e 223 | # .. ok .. 224 | 225 | parser.parse code.toString(), (e, css) => 226 | if e 227 | sys.puts " * Error compiling #{file}" 228 | sys.puts " #{e.message}" 229 | else 230 | sys.puts " * Compiled " + outpath 231 | fs.writeFileSync Path.join(@root, outpath), css.toCSS() 232 | 233 | # Compile xml fixtures 234 | # _compileXml : (file) -> 235 | # fs.readFile Path.join(@root, file), "utf-8", (err, code) => 236 | # throw err if err 237 | # 238 | # path = Path.join(Path.dirname(file), ".js") 239 | # outpath = Path.join(path, Path.basename(file, ".xml") + ".js") 240 | # 241 | # try 242 | # fs.mkdirSync Path.join(@root, path), 0755 243 | # catch e 244 | # # .. ok .. 245 | # 246 | # templateName = [ 247 | # Path.dirname(file).split("/").pop().toLowerCase() 248 | # Path.basename(file, ".xml").capitalize() 249 | # ].join("") 250 | # 251 | # output = "if(!this.$fixtures){\n $fixtures={};\n};\n\n" + 252 | # "this.$fixtures.#{templateName}=$(\"" + 253 | # code.replace(/"/g,"\\\"").replace(/\n/g,"\\n") + 254 | # "\");" 255 | # 256 | # sys.puts " * Compiled " + outpath 257 | # fs.writeFileSync Path.join(@root, outpath), output 258 | 259 | # Compile json fixtures 260 | # _compileJson : (file) -> 261 | # fs.readFile Path.join(@root, file), "utf-8", (err, code) => 262 | # throw err if err 263 | # 264 | # path = Path.join(Path.dirname(file), ".js") 265 | # outpath = Path.join(path, Path.basename(file, ".json") + ".js") 266 | # 267 | # try 268 | # fs.mkdirSync Path.join(@root, path), 0755 269 | # catch e 270 | # # .. ok .. 271 | # 272 | # templateName = [ 273 | # Path.dirname(file).split("/").pop().toLowerCase() 274 | # Path.basename(file, ".json").capitalize() 275 | # ].join("") 276 | # 277 | # output = "if(!this.$fixtures){\n $fixtures={};\n};\n\n" + 278 | # "this.$fixtures.#{templateName}=" + code.toString() + ";" 279 | # 280 | # sys.puts " * Compiled " + outpath 281 | # fs.writeFileSync Path.join(@root, outpath), output 282 | 283 | _compileCoffee : (file) -> 284 | fs.readFile Path.join(@root, file), (err, code) => 285 | throw err if err 286 | 287 | path = Path.join(Path.dirname(file), ".js") 288 | outpath = Path.join(path, Path.basename(file, ".coffee") + ".js") 289 | 290 | try 291 | fs.mkdirSync Path.join(@root, path), 0755 292 | catch e 293 | # .. ok .. 294 | 295 | try 296 | output = CoffeeScript.compile(new String(code)) 297 | catch err 298 | sys.puts " * Error compiling #{file}" 299 | sys.puts err.message 300 | return 301 | 302 | sys.puts " * Compiled " + outpath 303 | fs.writeFileSync Path.join(@root, outpath), output 304 | 305 | _compileEco : (file) -> 306 | fs.readFile Path.join(@root, file), "utf-8", (err, code) => 307 | throw err if err 308 | 309 | path = Path.join(Path.dirname(file), ".js") 310 | outpath = Path.join(path, Path.basename(file, ".eco") + ".js") 311 | 312 | try 313 | fs.mkdirSync Path.join(@root, path), 0755 314 | catch e 315 | # .. ok .. 316 | 317 | try 318 | output = eco.compile(new String(code)) 319 | catch err 320 | sys.puts " * Error compiling #{file}" 321 | sys.puts err.message 322 | return 323 | 324 | templateName = [ 325 | Path.dirname(file).split("/").pop().toLowerCase() 326 | Path.basename(file, ".eco").capitalize() 327 | ].join("") 328 | 329 | output = "this.$templates.#{templateName} = " + output 330 | output = "if(!this.$templates){\n $templates={};\n};\n\n" + output 331 | 332 | sys.puts " * Compiled " + outpath 333 | fs.writeFileSync Path.join(@root, outpath), output 334 | 335 | _compileJst : (file) -> 336 | fs.readFile Path.join(@root, file), (err, code) => 337 | throw err if err 338 | 339 | outpath = Path.join(Path.dirname(file), Path.basename(file, '.jst') + ".html") 340 | 341 | try 342 | output = _.template(new String(code), { project : this }) 343 | catch err 344 | sys.puts " * Error compiling #{file}" 345 | sys.puts err.message 346 | return 347 | 348 | sys.puts " * Compiled " + outpath 349 | fs.writeFileSync Path.join(@root, outpath), output 350 | 351 | build: -> 352 | for source in @getWatchables() 353 | @compileFile(source) 354 | 355 | watchAndBuild: -> 356 | watch = (source) => 357 | fs.watchFile Path.join(@root, source), {persistent: true, interval: 500}, (curr, prev) => 358 | return if curr.size is prev.size and curr.mtime.getTime() is prev.mtime.getTime() 359 | @compileFile(source) 360 | 361 | for source in @getWatchables() 362 | watch(source) 363 | @compileFile(source) 364 | 365 | exports.Project = Project 366 | -------------------------------------------------------------------------------- /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/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/config.yml: -------------------------------------------------------------------------------- 1 | 2 | javascripts: 3 | - 'lib/jquery.js' 4 | - 'lib/underscore.js' 5 | - 'lib/backbone.js' 6 | - 'app/controllers/*.coffee' 7 | - 'app/views/**/*.coffee' 8 | - 'app/models/*.coffee' 9 | 10 | stylesheets: 11 | - 'public/stylesheets/*.less' 12 | 13 | specs: 14 | - 'spec/*/*.coffee' 15 | - 'spec/views/*/*.coffee' 16 | 17 | fixtures: 18 | 19 | templates: 20 | - 'app/templates/*.eco' 21 | - 'app/templates/*/*.eco' -------------------------------------------------------------------------------- /templates/html/index.jst: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= project.name().capitalize() %> 4 | 5 | <%= project.stylesheetIncludes() %> 6 | <%= project.scriptIncludes() %> 7 | 8 | 9 | 10 | Hello world. 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/html/runner.jst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jasmine Test Runner 5 | 6 | 7 | 8 | 9 | <%= project.scriptIncludes() %> 10 | <%= project.specIncludes() %> 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 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 | 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, 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) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for **Node.js**, with 52 | // backwards-compatibility for the old `require()` API. If we're in 53 | // the browser, add `_` as a global object via a string identifier, 54 | // for Closure Compiler "advanced" mode. 55 | if (typeof exports !== 'undefined') { 56 | if (typeof module !== 'undefined' && module.exports) { 57 | exports = module.exports = _; 58 | } 59 | exports._ = _; 60 | } else { 61 | root['_'] = _; 62 | } 63 | 64 | // Current version. 65 | _.VERSION = '1.3.3'; 66 | 67 | // Collection Functions 68 | // -------------------- 69 | 70 | // The cornerstone, an `each` implementation, aka `forEach`. 71 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 72 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 73 | var each = _.each = _.forEach = function(obj, iterator, context) { 74 | if (obj == null) return; 75 | if (nativeForEach && obj.forEach === nativeForEach) { 76 | obj.forEach(iterator, context); 77 | } else if (obj.length === +obj.length) { 78 | for (var i = 0, l = obj.length; i < l; i++) { 79 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 80 | } 81 | } else { 82 | for (var key in obj) { 83 | if (_.has(obj, key)) { 84 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 85 | } 86 | } 87 | } 88 | }; 89 | 90 | // Return the results of applying the iterator to each element. 91 | // Delegates to **ECMAScript 5**'s native `map` if available. 92 | _.map = _.collect = function(obj, iterator, context) { 93 | var results = []; 94 | if (obj == null) return results; 95 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 96 | each(obj, function(value, index, list) { 97 | results[results.length] = iterator.call(context, value, index, list); 98 | }); 99 | if (obj.length === +obj.length) results.length = obj.length; 100 | return results; 101 | }; 102 | 103 | // **Reduce** builds up a single result from a list of values, aka `inject`, 104 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 105 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 106 | var initial = arguments.length > 2; 107 | if (obj == null) obj = []; 108 | if (nativeReduce && obj.reduce === nativeReduce) { 109 | if (context) iterator = _.bind(iterator, context); 110 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 111 | } 112 | each(obj, function(value, index, list) { 113 | if (!initial) { 114 | memo = value; 115 | initial = true; 116 | } else { 117 | memo = iterator.call(context, memo, value, index, list); 118 | } 119 | }); 120 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 121 | return memo; 122 | }; 123 | 124 | // The right-associative version of reduce, also known as `foldr`. 125 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 126 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 127 | var initial = arguments.length > 2; 128 | if (obj == null) obj = []; 129 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 130 | if (context) iterator = _.bind(iterator, context); 131 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 132 | } 133 | var reversed = _.toArray(obj).reverse(); 134 | if (context && !initial) iterator = _.bind(iterator, context); 135 | return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); 136 | }; 137 | 138 | // Return the first value which passes a truth test. Aliased as `detect`. 139 | _.find = _.detect = function(obj, iterator, context) { 140 | var result; 141 | any(obj, function(value, index, list) { 142 | if (iterator.call(context, value, index, list)) { 143 | result = value; 144 | return true; 145 | } 146 | }); 147 | return result; 148 | }; 149 | 150 | // Return all the elements that pass a truth test. 151 | // Delegates to **ECMAScript 5**'s native `filter` if available. 152 | // Aliased as `select`. 153 | _.filter = _.select = function(obj, iterator, context) { 154 | var results = []; 155 | if (obj == null) return results; 156 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 157 | each(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) results[results.length] = value; 159 | }); 160 | return results; 161 | }; 162 | 163 | // Return all the elements for which a truth test fails. 164 | _.reject = function(obj, iterator, context) { 165 | var results = []; 166 | if (obj == null) return results; 167 | each(obj, function(value, index, list) { 168 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 169 | }); 170 | return results; 171 | }; 172 | 173 | // Determine whether all of the elements match a truth test. 174 | // Delegates to **ECMAScript 5**'s native `every` if available. 175 | // Aliased as `all`. 176 | _.every = _.all = function(obj, iterator, context) { 177 | var result = true; 178 | if (obj == null) return result; 179 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 180 | each(obj, function(value, index, list) { 181 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 182 | }); 183 | return !!result; 184 | }; 185 | 186 | // Determine if at least one element in the object matches a truth test. 187 | // Delegates to **ECMAScript 5**'s native `some` if available. 188 | // Aliased as `any`. 189 | var any = _.some = _.any = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = false; 192 | if (obj == null) return result; 193 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if a given value is included in the array or object using `===`. 201 | // Aliased as `contains`. 202 | _.include = _.contains = function(obj, target) { 203 | var found = false; 204 | if (obj == null) return found; 205 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 206 | found = any(obj, function(value) { 207 | return value === target; 208 | }); 209 | return found; 210 | }; 211 | 212 | // Invoke a method (with arguments) on every item in a collection. 213 | _.invoke = function(obj, method) { 214 | var args = slice.call(arguments, 2); 215 | return _.map(obj, function(value) { 216 | return (_.isFunction(method) ? method || value : value[method]).apply(value, args); 217 | }); 218 | }; 219 | 220 | // Convenience version of a common use case of `map`: fetching a property. 221 | _.pluck = function(obj, key) { 222 | return _.map(obj, function(value){ return value[key]; }); 223 | }; 224 | 225 | // Return the maximum element or (element-based computation). 226 | _.max = function(obj, iterator, context) { 227 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj); 228 | if (!iterator && _.isEmpty(obj)) return -Infinity; 229 | var result = {computed : -Infinity}; 230 | each(obj, function(value, index, list) { 231 | var computed = iterator ? iterator.call(context, value, index, list) : value; 232 | computed >= result.computed && (result = {value : value, computed : computed}); 233 | }); 234 | return result.value; 235 | }; 236 | 237 | // Return the minimum element (or element-based computation). 238 | _.min = function(obj, iterator, context) { 239 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj); 240 | if (!iterator && _.isEmpty(obj)) return Infinity; 241 | var result = {computed : Infinity}; 242 | each(obj, function(value, index, list) { 243 | var computed = iterator ? iterator.call(context, value, index, list) : value; 244 | computed < result.computed && (result = {value : value, computed : computed}); 245 | }); 246 | return result.value; 247 | }; 248 | 249 | // Shuffle an array. 250 | _.shuffle = function(obj) { 251 | var shuffled = [], rand; 252 | each(obj, function(value, index, list) { 253 | rand = Math.floor(Math.random() * (index + 1)); 254 | shuffled[index] = shuffled[rand]; 255 | shuffled[rand] = value; 256 | }); 257 | return shuffled; 258 | }; 259 | 260 | // Sort the object's values by a criterion produced by an iterator. 261 | _.sortBy = function(obj, val, context) { 262 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 263 | return _.pluck(_.map(obj, function(value, index, list) { 264 | return { 265 | value : value, 266 | criteria : iterator.call(context, value, index, list) 267 | }; 268 | }).sort(function(left, right) { 269 | var a = left.criteria, b = right.criteria; 270 | if (a === void 0) return 1; 271 | if (b === void 0) return -1; 272 | return a < b ? -1 : a > b ? 1 : 0; 273 | }), 'value'); 274 | }; 275 | 276 | // Groups the object's values by a criterion. Pass either a string attribute 277 | // to group by, or a function that returns the criterion. 278 | _.groupBy = function(obj, val) { 279 | var result = {}; 280 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 281 | each(obj, function(value, index) { 282 | var key = iterator(value, index); 283 | (result[key] || (result[key] = [])).push(value); 284 | }); 285 | return result; 286 | }; 287 | 288 | // Use a comparator function to figure out at what index an object should 289 | // be inserted so as to maintain order. Uses binary search. 290 | _.sortedIndex = function(array, obj, iterator) { 291 | iterator || (iterator = _.identity); 292 | var low = 0, high = array.length; 293 | while (low < high) { 294 | var mid = (low + high) >> 1; 295 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 296 | } 297 | return low; 298 | }; 299 | 300 | // Safely convert anything iterable into a real, live array. 301 | _.toArray = function(obj) { 302 | if (!obj) return []; 303 | if (_.isArray(obj)) return slice.call(obj); 304 | if (_.isArguments(obj)) return slice.call(obj); 305 | if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray(); 306 | return _.values(obj); 307 | }; 308 | 309 | // Return the number of elements in an object. 310 | _.size = function(obj) { 311 | return _.isArray(obj) ? obj.length : _.keys(obj).length; 312 | }; 313 | 314 | // Array Functions 315 | // --------------- 316 | 317 | // Get the first element of an array. Passing **n** will return the first N 318 | // values in the array. Aliased as `head` and `take`. The **guard** check 319 | // allows it to work with `_.map`. 320 | _.first = _.head = _.take = function(array, n, guard) { 321 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 322 | }; 323 | 324 | // Returns everything but the last entry of the array. Especcialy useful on 325 | // the arguments object. Passing **n** will return all the values in 326 | // the array, excluding the last N. The **guard** check allows it to work with 327 | // `_.map`. 328 | _.initial = function(array, n, guard) { 329 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 330 | }; 331 | 332 | // Get the last element of an array. Passing **n** will return the last N 333 | // values in the array. The **guard** check allows it to work with `_.map`. 334 | _.last = function(array, n, guard) { 335 | if ((n != null) && !guard) { 336 | return slice.call(array, Math.max(array.length - n, 0)); 337 | } else { 338 | return array[array.length - 1]; 339 | } 340 | }; 341 | 342 | // Returns everything but the first entry of the array. Aliased as `tail`. 343 | // Especially useful on the arguments object. Passing an **index** will return 344 | // the rest of the values in the array from that index onward. The **guard** 345 | // check allows it to work with `_.map`. 346 | _.rest = _.tail = function(array, index, guard) { 347 | return slice.call(array, (index == null) || guard ? 1 : index); 348 | }; 349 | 350 | // Trim out all falsy values from an array. 351 | _.compact = function(array) { 352 | return _.filter(array, function(value){ return !!value; }); 353 | }; 354 | 355 | // Return a completely flattened version of an array. 356 | _.flatten = function(array, shallow) { 357 | return _.reduce(array, function(memo, value) { 358 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 359 | memo[memo.length] = value; 360 | return memo; 361 | }, []); 362 | }; 363 | 364 | // Return a version of the array that does not contain the specified value(s). 365 | _.without = function(array) { 366 | return _.difference(array, slice.call(arguments, 1)); 367 | }; 368 | 369 | // Produce a duplicate-free version of the array. If the array has already 370 | // been sorted, you have the option of using a faster algorithm. 371 | // Aliased as `unique`. 372 | _.uniq = _.unique = function(array, isSorted, iterator) { 373 | var initial = iterator ? _.map(array, iterator) : array; 374 | var results = []; 375 | // The `isSorted` flag is irrelevant if the array only contains two elements. 376 | if (array.length < 3) isSorted = true; 377 | _.reduce(initial, function (memo, value, index) { 378 | if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) { 379 | memo.push(value); 380 | results.push(array[index]); 381 | } 382 | return memo; 383 | }, []); 384 | return results; 385 | }; 386 | 387 | // Produce an array that contains the union: each distinct element from all of 388 | // the passed-in arrays. 389 | _.union = function() { 390 | return _.uniq(_.flatten(arguments, true)); 391 | }; 392 | 393 | // Produce an array that contains every item shared between all the 394 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 395 | _.intersection = _.intersect = function(array) { 396 | var rest = slice.call(arguments, 1); 397 | return _.filter(_.uniq(array), function(item) { 398 | return _.every(rest, function(other) { 399 | return _.indexOf(other, item) >= 0; 400 | }); 401 | }); 402 | }; 403 | 404 | // Take the difference between one array and a number of other arrays. 405 | // Only the elements present in just the first array will remain. 406 | _.difference = function(array) { 407 | var rest = _.flatten(slice.call(arguments, 1), true); 408 | return _.filter(array, function(value){ return !_.include(rest, value); }); 409 | }; 410 | 411 | // Zip together multiple lists into a single array -- elements that share 412 | // an index go together. 413 | _.zip = function() { 414 | var args = slice.call(arguments); 415 | var length = _.max(_.pluck(args, 'length')); 416 | var results = new Array(length); 417 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 418 | return results; 419 | }; 420 | 421 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 422 | // we need this function. Return the position of the first occurrence of an 423 | // item in an array, or -1 if the item is not included in the array. 424 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 425 | // If the array is large and already in sort order, pass `true` 426 | // for **isSorted** to use binary search. 427 | _.indexOf = function(array, item, isSorted) { 428 | if (array == null) return -1; 429 | var i, l; 430 | if (isSorted) { 431 | i = _.sortedIndex(array, item); 432 | return array[i] === item ? i : -1; 433 | } 434 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 435 | for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; 436 | return -1; 437 | }; 438 | 439 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 440 | _.lastIndexOf = function(array, item) { 441 | if (array == null) return -1; 442 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 443 | var i = array.length; 444 | while (i--) if (i in array && array[i] === item) return i; 445 | return -1; 446 | }; 447 | 448 | // Generate an integer Array containing an arithmetic progression. A port of 449 | // the native Python `range()` function. See 450 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 451 | _.range = function(start, stop, step) { 452 | if (arguments.length <= 1) { 453 | stop = start || 0; 454 | start = 0; 455 | } 456 | step = arguments[2] || 1; 457 | 458 | var len = Math.max(Math.ceil((stop - start) / step), 0); 459 | var idx = 0; 460 | var range = new Array(len); 461 | 462 | while(idx < len) { 463 | range[idx++] = start; 464 | start += step; 465 | } 466 | 467 | return range; 468 | }; 469 | 470 | // Function (ahem) Functions 471 | // ------------------ 472 | 473 | // Reusable constructor function for prototype setting. 474 | var ctor = function(){}; 475 | 476 | // Create a function bound to a given object (assigning `this`, and arguments, 477 | // optionally). Binding with arguments is also known as `curry`. 478 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 479 | // We check for `func.bind` first, to fail fast when `func` is undefined. 480 | _.bind = function bind(func, context) { 481 | var bound, args; 482 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 483 | if (!_.isFunction(func)) throw new TypeError; 484 | args = slice.call(arguments, 2); 485 | return bound = function() { 486 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 487 | ctor.prototype = func.prototype; 488 | var self = new ctor; 489 | var result = func.apply(self, args.concat(slice.call(arguments))); 490 | if (Object(result) === result) return result; 491 | return self; 492 | }; 493 | }; 494 | 495 | // Bind all of an object's methods to that object. Useful for ensuring that 496 | // all callbacks defined on an object belong to it. 497 | _.bindAll = function(obj) { 498 | var funcs = slice.call(arguments, 1); 499 | if (funcs.length == 0) funcs = _.functions(obj); 500 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 501 | return obj; 502 | }; 503 | 504 | // Memoize an expensive function by storing its results. 505 | _.memoize = function(func, hasher) { 506 | var memo = {}; 507 | hasher || (hasher = _.identity); 508 | return function() { 509 | var key = hasher.apply(this, arguments); 510 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 511 | }; 512 | }; 513 | 514 | // Delays a function for the given number of milliseconds, and then calls 515 | // it with the arguments supplied. 516 | _.delay = function(func, wait) { 517 | var args = slice.call(arguments, 2); 518 | return setTimeout(function(){ return func.apply(null, args); }, wait); 519 | }; 520 | 521 | // Defers a function, scheduling it to run after the current call stack has 522 | // cleared. 523 | _.defer = function(func) { 524 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 525 | }; 526 | 527 | // Returns a function, that, when invoked, will only be triggered at most once 528 | // during a given window of time. 529 | _.throttle = function(func, wait) { 530 | var context, args, timeout, throttling, more, result; 531 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 532 | return function() { 533 | context = this; args = arguments; 534 | var later = function() { 535 | timeout = null; 536 | if (more) func.apply(context, args); 537 | whenDone(); 538 | }; 539 | if (!timeout) timeout = setTimeout(later, wait); 540 | if (throttling) { 541 | more = true; 542 | } else { 543 | result = func.apply(context, args); 544 | } 545 | whenDone(); 546 | throttling = true; 547 | return result; 548 | }; 549 | }; 550 | 551 | // Returns a function, that, as long as it continues to be invoked, will not 552 | // be triggered. The function will be called after it stops being called for 553 | // N milliseconds. If `immediate` is passed, trigger the function on the 554 | // leading edge, instead of the trailing. 555 | _.debounce = function(func, wait, immediate) { 556 | var timeout; 557 | return function() { 558 | var context = this, args = arguments; 559 | var later = function() { 560 | timeout = null; 561 | if (!immediate) func.apply(context, args); 562 | }; 563 | if (immediate && !timeout) func.apply(context, args); 564 | clearTimeout(timeout); 565 | timeout = setTimeout(later, wait); 566 | }; 567 | }; 568 | 569 | // Returns a function that will be executed at most one time, no matter how 570 | // often you call it. Useful for lazy initialization. 571 | _.once = function(func) { 572 | var ran = false, memo; 573 | return function() { 574 | if (ran) return memo; 575 | ran = true; 576 | return memo = func.apply(this, arguments); 577 | }; 578 | }; 579 | 580 | // Returns the first function passed as an argument to the second, 581 | // allowing you to adjust arguments, run code before and after, and 582 | // conditionally execute the original function. 583 | _.wrap = function(func, wrapper) { 584 | return function() { 585 | var args = [func].concat(slice.call(arguments, 0)); 586 | return wrapper.apply(this, args); 587 | }; 588 | }; 589 | 590 | // Returns a function that is the composition of a list of functions, each 591 | // consuming the return value of the function that follows. 592 | _.compose = function() { 593 | var funcs = arguments; 594 | return function() { 595 | var args = arguments; 596 | for (var i = funcs.length - 1; i >= 0; i--) { 597 | args = [funcs[i].apply(this, args)]; 598 | } 599 | return args[0]; 600 | }; 601 | }; 602 | 603 | // Returns a function that will only be executed after being called N times. 604 | _.after = function(times, func) { 605 | if (times <= 0) return func(); 606 | return function() { 607 | if (--times < 1) { return func.apply(this, arguments); } 608 | }; 609 | }; 610 | 611 | // Object Functions 612 | // ---------------- 613 | 614 | // Retrieve the names of an object's properties. 615 | // Delegates to **ECMAScript 5**'s native `Object.keys` 616 | _.keys = nativeKeys || function(obj) { 617 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 618 | var keys = []; 619 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 620 | return keys; 621 | }; 622 | 623 | // Retrieve the values of an object's properties. 624 | _.values = function(obj) { 625 | return _.map(obj, _.identity); 626 | }; 627 | 628 | // Return a sorted list of the function names available on the object. 629 | // Aliased as `methods` 630 | _.functions = _.methods = function(obj) { 631 | var names = []; 632 | for (var key in obj) { 633 | if (_.isFunction(obj[key])) names.push(key); 634 | } 635 | return names.sort(); 636 | }; 637 | 638 | // Extend a given object with all the properties in passed-in object(s). 639 | _.extend = function(obj) { 640 | each(slice.call(arguments, 1), function(source) { 641 | for (var prop in source) { 642 | obj[prop] = source[prop]; 643 | } 644 | }); 645 | return obj; 646 | }; 647 | 648 | // Return a copy of the object only containing the whitelisted properties. 649 | _.pick = function(obj) { 650 | var result = {}; 651 | each(_.flatten(slice.call(arguments, 1)), function(key) { 652 | if (key in obj) result[key] = obj[key]; 653 | }); 654 | return result; 655 | }; 656 | 657 | // Fill in a given object with default properties. 658 | _.defaults = function(obj) { 659 | each(slice.call(arguments, 1), function(source) { 660 | for (var prop in source) { 661 | if (obj[prop] == null) obj[prop] = source[prop]; 662 | } 663 | }); 664 | return obj; 665 | }; 666 | 667 | // Create a (shallow-cloned) duplicate of an object. 668 | _.clone = function(obj) { 669 | if (!_.isObject(obj)) return obj; 670 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 671 | }; 672 | 673 | // Invokes interceptor with the obj, and then returns obj. 674 | // The primary purpose of this method is to "tap into" a method chain, in 675 | // order to perform operations on intermediate results within the chain. 676 | _.tap = function(obj, interceptor) { 677 | interceptor(obj); 678 | return obj; 679 | }; 680 | 681 | // Internal recursive comparison function. 682 | function eq(a, b, stack) { 683 | // Identical objects are equal. `0 === -0`, but they aren't identical. 684 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 685 | if (a === b) return a !== 0 || 1 / a == 1 / b; 686 | // A strict comparison is necessary because `null == undefined`. 687 | if (a == null || b == null) return a === b; 688 | // Unwrap any wrapped objects. 689 | if (a._chain) a = a._wrapped; 690 | if (b._chain) b = b._wrapped; 691 | // Invoke a custom `isEqual` method if one is provided. 692 | if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); 693 | if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); 694 | // Compare `[[Class]]` names. 695 | var className = toString.call(a); 696 | if (className != toString.call(b)) return false; 697 | switch (className) { 698 | // Strings, numbers, dates, and booleans are compared by value. 699 | case '[object String]': 700 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 701 | // equivalent to `new String("5")`. 702 | return a == String(b); 703 | case '[object Number]': 704 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 705 | // other numeric values. 706 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 707 | case '[object Date]': 708 | case '[object Boolean]': 709 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 710 | // millisecond representations. Note that invalid dates with millisecond representations 711 | // of `NaN` are not equivalent. 712 | return +a == +b; 713 | // RegExps are compared by their source patterns and flags. 714 | case '[object RegExp]': 715 | return a.source == b.source && 716 | a.global == b.global && 717 | a.multiline == b.multiline && 718 | a.ignoreCase == b.ignoreCase; 719 | } 720 | if (typeof a != 'object' || typeof b != 'object') return false; 721 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 722 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 723 | var length = stack.length; 724 | while (length--) { 725 | // Linear search. Performance is inversely proportional to the number of 726 | // unique nested structures. 727 | if (stack[length] == a) return true; 728 | } 729 | // Add the first object to the stack of traversed objects. 730 | stack.push(a); 731 | var size = 0, result = true; 732 | // Recursively compare objects and arrays. 733 | if (className == '[object Array]') { 734 | // Compare array lengths to determine if a deep comparison is necessary. 735 | size = a.length; 736 | result = size == b.length; 737 | if (result) { 738 | // Deep compare the contents, ignoring non-numeric properties. 739 | while (size--) { 740 | // Ensure commutative equality for sparse arrays. 741 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 742 | } 743 | } 744 | } else { 745 | // Objects with different constructors are not equivalent. 746 | if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; 747 | // Deep compare objects. 748 | for (var key in a) { 749 | if (_.has(a, key)) { 750 | // Count the expected number of properties. 751 | size++; 752 | // Deep compare each member. 753 | if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; 754 | } 755 | } 756 | // Ensure that both objects contain the same number of properties. 757 | if (result) { 758 | for (key in b) { 759 | if (_.has(b, key) && !(size--)) break; 760 | } 761 | result = !size; 762 | } 763 | } 764 | // Remove the first object from the stack of traversed objects. 765 | stack.pop(); 766 | return result; 767 | } 768 | 769 | // Perform a deep comparison to check if two objects are equal. 770 | _.isEqual = function(a, b) { 771 | return eq(a, b, []); 772 | }; 773 | 774 | // Is a given array, string, or object empty? 775 | // An "empty" object has no enumerable own-properties. 776 | _.isEmpty = function(obj) { 777 | if (obj == null) return true; 778 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 779 | for (var key in obj) if (_.has(obj, key)) return false; 780 | return true; 781 | }; 782 | 783 | // Is a given value a DOM element? 784 | _.isElement = function(obj) { 785 | return !!(obj && obj.nodeType == 1); 786 | }; 787 | 788 | // Is a given value an array? 789 | // Delegates to ECMA5's native Array.isArray 790 | _.isArray = nativeIsArray || function(obj) { 791 | return toString.call(obj) == '[object Array]'; 792 | }; 793 | 794 | // Is a given variable an object? 795 | _.isObject = function(obj) { 796 | return obj === Object(obj); 797 | }; 798 | 799 | // Is a given variable an arguments object? 800 | _.isArguments = function(obj) { 801 | return toString.call(obj) == '[object Arguments]'; 802 | }; 803 | if (!_.isArguments(arguments)) { 804 | _.isArguments = function(obj) { 805 | return !!(obj && _.has(obj, 'callee')); 806 | }; 807 | } 808 | 809 | // Is a given value a function? 810 | _.isFunction = function(obj) { 811 | return toString.call(obj) == '[object Function]'; 812 | }; 813 | 814 | // Is a given value a string? 815 | _.isString = function(obj) { 816 | return toString.call(obj) == '[object String]'; 817 | }; 818 | 819 | // Is a given value a number? 820 | _.isNumber = function(obj) { 821 | return toString.call(obj) == '[object Number]'; 822 | }; 823 | 824 | // Is a given object a finite number? 825 | _.isFinite = function(obj) { 826 | return _.isNumber(obj) && isFinite(obj); 827 | }; 828 | 829 | // Is the given value `NaN`? 830 | _.isNaN = function(obj) { 831 | // `NaN` is the only value for which `===` is not reflexive. 832 | return obj !== obj; 833 | }; 834 | 835 | // Is a given value a boolean? 836 | _.isBoolean = function(obj) { 837 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 838 | }; 839 | 840 | // Is a given value a date? 841 | _.isDate = function(obj) { 842 | return toString.call(obj) == '[object Date]'; 843 | }; 844 | 845 | // Is the given value a regular expression? 846 | _.isRegExp = function(obj) { 847 | return toString.call(obj) == '[object RegExp]'; 848 | }; 849 | 850 | // Is a given value equal to null? 851 | _.isNull = function(obj) { 852 | return obj === null; 853 | }; 854 | 855 | // Is a given variable undefined? 856 | _.isUndefined = function(obj) { 857 | return obj === void 0; 858 | }; 859 | 860 | // Has own property? 861 | _.has = function(obj, key) { 862 | return hasOwnProperty.call(obj, key); 863 | }; 864 | 865 | // Utility Functions 866 | // ----------------- 867 | 868 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 869 | // previous owner. Returns a reference to the Underscore object. 870 | _.noConflict = function() { 871 | root._ = previousUnderscore; 872 | return this; 873 | }; 874 | 875 | // Keep the identity function around for default iterators. 876 | _.identity = function(value) { 877 | return value; 878 | }; 879 | 880 | // Run a function **n** times. 881 | _.times = function (n, iterator, context) { 882 | for (var i = 0; i < n; i++) iterator.call(context, i); 883 | }; 884 | 885 | // Escape a string for HTML interpolation. 886 | _.escape = function(string) { 887 | return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); 888 | }; 889 | 890 | // If the value of the named property is a function then invoke it; 891 | // otherwise, return it. 892 | _.result = function(object, property) { 893 | if (object == null) return null; 894 | var value = object[property]; 895 | return _.isFunction(value) ? value.call(object) : value; 896 | }; 897 | 898 | // Add your own custom functions to the Underscore object, ensuring that 899 | // they're correctly added to the OOP wrapper as well. 900 | _.mixin = function(obj) { 901 | each(_.functions(obj), function(name){ 902 | addToWrapper(name, _[name] = obj[name]); 903 | }); 904 | }; 905 | 906 | // Generate a unique integer id (unique within the entire client session). 907 | // Useful for temporary DOM ids. 908 | var idCounter = 0; 909 | _.uniqueId = function(prefix) { 910 | var id = idCounter++; 911 | return prefix ? prefix + id : id; 912 | }; 913 | 914 | // By default, Underscore uses ERB-style template delimiters, change the 915 | // following template settings to use alternative delimiters. 916 | _.templateSettings = { 917 | evaluate : /<%([\s\S]+?)%>/g, 918 | interpolate : /<%=([\s\S]+?)%>/g, 919 | escape : /<%-([\s\S]+?)%>/g 920 | }; 921 | 922 | // When customizing `templateSettings`, if you don't want to define an 923 | // interpolation, evaluation or escaping regex, we need one that is 924 | // guaranteed not to match. 925 | var noMatch = /.^/; 926 | 927 | // Certain characters need to be escaped so that they can be put into a 928 | // string literal. 929 | var escapes = { 930 | '\\': '\\', 931 | "'": "'", 932 | 'r': '\r', 933 | 'n': '\n', 934 | 't': '\t', 935 | 'u2028': '\u2028', 936 | 'u2029': '\u2029' 937 | }; 938 | 939 | for (var p in escapes) escapes[escapes[p]] = p; 940 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 941 | var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; 942 | 943 | // Within an interpolation, evaluation, or escaping, remove HTML escaping 944 | // that had been previously added. 945 | var unescape = function(code) { 946 | return code.replace(unescaper, function(match, escape) { 947 | return escapes[escape]; 948 | }); 949 | }; 950 | 951 | // JavaScript micro-templating, similar to John Resig's implementation. 952 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 953 | // and correctly escapes quotes within interpolated code. 954 | _.template = function(text, data, settings) { 955 | settings = _.defaults(settings || {}, _.templateSettings); 956 | 957 | // Compile the template source, taking care to escape characters that 958 | // cannot be included in a string literal and then unescape them in code 959 | // blocks. 960 | var source = "__p+='" + text 961 | .replace(escaper, function(match) { 962 | return '\\' + escapes[match]; 963 | }) 964 | .replace(settings.escape || noMatch, function(match, code) { 965 | return "'+\n_.escape(" + unescape(code) + ")+\n'"; 966 | }) 967 | .replace(settings.interpolate || noMatch, function(match, code) { 968 | return "'+\n(" + unescape(code) + ")+\n'"; 969 | }) 970 | .replace(settings.evaluate || noMatch, function(match, code) { 971 | return "';\n" + unescape(code) + "\n;__p+='"; 972 | }) + "';\n"; 973 | 974 | // If a variable is not specified, place data values in local scope. 975 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 976 | 977 | source = "var __p='';" + 978 | "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + 979 | source + "return __p;\n"; 980 | 981 | var render = new Function(settings.variable || 'obj', '_', source); 982 | if (data) return render(data, _); 983 | var template = function(data) { 984 | return render.call(this, data, _); 985 | }; 986 | 987 | // Provide the compiled function source as a convenience for build time 988 | // precompilation. 989 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + 990 | source + '}'; 991 | 992 | return template; 993 | }; 994 | 995 | // Add a "chain" function, which will delegate to the wrapper. 996 | _.chain = function(obj) { 997 | return _(obj).chain(); 998 | }; 999 | 1000 | // The OOP Wrapper 1001 | // --------------- 1002 | 1003 | // If Underscore is called as a function, it returns a wrapped object that 1004 | // can be used OO-style. This wrapper holds altered versions of all the 1005 | // underscore functions. Wrapped objects may be chained. 1006 | var wrapper = function(obj) { this._wrapped = obj; }; 1007 | 1008 | // Expose `wrapper.prototype` as `_.prototype` 1009 | _.prototype = wrapper.prototype; 1010 | 1011 | // Helper function to continue chaining intermediate results. 1012 | var result = function(obj, chain) { 1013 | return chain ? _(obj).chain() : obj; 1014 | }; 1015 | 1016 | // A method to easily add functions to the OOP wrapper. 1017 | var addToWrapper = function(name, func) { 1018 | wrapper.prototype[name] = function() { 1019 | var args = slice.call(arguments); 1020 | unshift.call(args, this._wrapped); 1021 | return result(func.apply(_, args), this._chain); 1022 | }; 1023 | }; 1024 | 1025 | // Add all of the Underscore functions to the wrapper object. 1026 | _.mixin(_); 1027 | 1028 | // Add all mutator Array functions to the wrapper. 1029 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1030 | var method = ArrayProto[name]; 1031 | wrapper.prototype[name] = function() { 1032 | var wrapped = this._wrapped; 1033 | method.apply(wrapped, arguments); 1034 | var length = wrapped.length; 1035 | if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; 1036 | return result(wrapped, this._chain); 1037 | }; 1038 | }); 1039 | 1040 | // Add all accessor Array functions to the wrapper. 1041 | each(['concat', 'join', 'slice'], function(name) { 1042 | var method = ArrayProto[name]; 1043 | wrapper.prototype[name] = function() { 1044 | return result(method.apply(this._wrapped, arguments), this._chain); 1045 | }; 1046 | }); 1047 | 1048 | // Start chaining a wrapped Underscore object. 1049 | wrapper.prototype.chain = function() { 1050 | this._chain = true; 1051 | return this; 1052 | }; 1053 | 1054 | // Extracts the result from a wrapped and chained object. 1055 | wrapper.prototype.value = function() { 1056 | return this._wrapped; 1057 | }; 1058 | 1059 | }).call(this); -------------------------------------------------------------------------------- /templates/models/model.coffee: -------------------------------------------------------------------------------- 1 | class <%= model.capitalize() %> extends Backbone.Model 2 | initialize: -> 3 | # ... 4 | 5 | @<%= model.capitalize() %> = <%= model.capitalize() %> 6 | -------------------------------------------------------------------------------- /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/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/routers/router.coffee: -------------------------------------------------------------------------------- 1 | class <%= router.capitalize() %>Router extends Backbone.Router 2 | routes : 3 | "<%= router %>/:id/edit" : "edit" 4 | "<%= router %>/new" : "new" 5 | "<%= router %>/:id" : "show" 6 | "<%= router %>" : "index" 7 | 8 | index: -> 9 | # new <%= router.capitalize() %>IndexView 10 | 11 | @<%= router.capitalize() %>Router = <%= router.capitalize() %>Router 12 | -------------------------------------------------------------------------------- /templates/routers/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= router %> router', -> 2 | 3 | it 'should handle the truth', -> 4 | expect(true).toBeTruthy() 5 | 6 | it 'should exist', -> 7 | expect(<%= router.capitalize() %>Router).toBeTruthy() 8 | 9 | it 'should instantiate', -> 10 | x = new <%= router.capitalize() %>Controller 11 | expect(x instanceof <%= router.capitalize() %>Router).toBeTruthy() 12 | expect(x instanceof Backbone.Router).toBeTruthy() 13 | 14 | it 'should have index method', -> 15 | x = new <%= router.capitalize() %>Router 16 | x.index() 17 | 18 | # Umm..? 19 | expect(true).toBeTruthy() 20 | -------------------------------------------------------------------------------- /templates/templates/template.eco: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/views/spec.coffee: -------------------------------------------------------------------------------- 1 | describe '<%= router %> router', -> 2 | 3 | describe '<%= view %> view', -> 4 | 5 | it 'should handle the truth', -> 6 | expect(true).toBeTruthy() 7 | 8 | it 'should exist', -> 9 | expect(<%= router.capitalize() %><%= view.capitalize() %>View).toBeTruthy() 10 | 11 | it 'should instantiate', -> 12 | x = new <%= router.capitalize() %><%= view.capitalize() %>View 13 | expect(x instanceof <%= router.capitalize() %><%= view.capitalize() %>View).toBeTruthy() 14 | expect(x instanceof Backbone.View).toBeTruthy() 15 | 16 | it 'should have render method', -> 17 | x = new <%= router.capitalize() %><%= view.capitalize() %>View 18 | x.render() 19 | 20 | # Implement as you see fit 21 | xit 'should render some text', -> 22 | x = new <%= router.capitalize() %><%= view.capitalize() %>View { el : $("
") } 23 | x.render() 24 | expect(x.$(".myselector").html()).toMatch /some text/ 25 | -------------------------------------------------------------------------------- /templates/views/view.coffee: -------------------------------------------------------------------------------- 1 | class <%= router.capitalize() %><%= view.capitalize() %>View extends Backbone.View 2 | initialize: -> 3 | 4 | render: -> 5 | $(@el).html("blah!") 6 | 7 | @<%= router.capitalize() %><%= view.capitalize() %>View = <%= router.capitalize() %><%= view.capitalize() %>View 8 | --------------------------------------------------------------------------------