├── .gitignore ├── README.md ├── Vagrantfile ├── license ├── provisioning ├── ansible_hosts ├── etcd-nginx.yml ├── nginx.etcd.conf.j2 └── playbook.yml └── public ├── css └── style.css ├── index.html └── js ├── app.js └── libs ├── ember-1.0.0.js ├── handlebars-1.0.0.js └── jquery-1.9.1.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE:** Etcd is now distributed with its own web interface. Look at the [dashboard module](https://github.com/coreos/etcd/tree/master/mod/dashboard) for more details. 2 | 3 | A simple UI for [etcd](https://github.com/coreos/etcd) 4 | 5 | ## Running without Vagrant 6 | 7 | First start etcd with CORS enabled for globally: 8 | 9 | ``` 10 | etcd -cors='*' 11 | ``` 12 | 13 | Then start a simple server at the base of the wetcd directory. 14 | 15 | ``` 16 | cd public 17 | python -m SimpleHTTPServer 8000 18 | open http://127.0.0.1:8000 19 | ``` 20 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "precise64" 9 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 10 | 11 | config.vm.network :private_network, ip: "192.168.10.10" 12 | 13 | config.vm.synced_folder ".", "/var/www/wetcd" 14 | 15 | config.vm.provision :ansible do |ansible| 16 | ansible.sudo = true 17 | ansible.verbose = 'v' 18 | ansible.inventory_path = "provisioning/ansible_hosts" 19 | ansible.playbook = "provisioning/playbook.yml" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ben Scofield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /provisioning/ansible_hosts: -------------------------------------------------------------------------------- 1 | [vagrant] 2 | 192.168.10.10 -------------------------------------------------------------------------------- /provisioning/etcd-nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set up nginx to proxy to etcd 3 | hosts: vagrant 4 | tasks: 5 | - name: Replace default nginx config 6 | template: src=nginx.etcd.conf.j2 7 | dest=/etc/nginx/sites-available/site 8 | 9 | - name: Restart nginx 10 | command: restart nginx -------------------------------------------------------------------------------- /provisioning/nginx.etcd.conf.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen *:80; 3 | 4 | server_name {{ app }}; 5 | root /var/www/{{ app }}/public; 6 | 7 | client_max_body_size 10000k; 8 | 9 | location / { 10 | access_log /var/log/nginx/{{ app }}.log; 11 | 12 | root /var/www/{{ app }}/public; 13 | index index.html; 14 | } 15 | 16 | location ^~ /v1/keys { 17 | access_log /var/log/nginx/etcd.log; 18 | 19 | proxy_pass http://etcd; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_set_header Host $http_host; 23 | proxy_redirect off; 24 | } 25 | } 26 | 27 | upstream etcd { 28 | server {{ ip }}:4001; 29 | } 30 | -------------------------------------------------------------------------------- /provisioning/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set up VM for wetcd 3 | hosts: vagrant 4 | 5 | - include: "~/Projects/ansible/etcd.yml" 6 | vars: 7 | ip: 192.168.10.10 8 | 9 | - include: "~/Projects/ansible/nginx.yml" 10 | vars: 11 | app: wetcd 12 | 13 | - include: "etcd-nginx.yml" 14 | vars: 15 | app: wetcd 16 | ip: 192.168.10.10 17 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | td { 5 | min-height: 30px; 6 | padding: 5px 1em; 7 | word-wrap: break-word; 8 | } 9 | td button, td a, td span { 10 | line-height: 30px; 11 | } 12 | .key-field, td.key { 13 | text-align: right; 14 | } 15 | td.key em { 16 | display: block; 17 | font-weight: normal; 18 | } 19 | table.edit-true { 20 | background-color: #ffe; 21 | } 22 | 23 | #add-key-button { 24 | padding-top: 10px; 25 | } 26 | #del-button { 27 | margin-left: 1em; 28 | } 29 | body h2 small { 30 | font-size: 12px; 31 | } 32 | 33 | .btn-txt { 34 | background-color: transparent; 35 | border: none; 36 | font-weight: bold; 37 | margin: 0; 38 | padding: 0; 39 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wetcd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 29 | 30 | 69 | 70 | 77 | 78 | 99 | 100 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | Wetcd = Ember.Application.create({}); 2 | 3 | Wetcd.Router.map(function() { 4 | this.resource('keys', { path: '*key' }); 5 | }); 6 | 7 | Wetcd.Key = Ember.Object.extend({ 8 | deleted: false, 9 | editing: false, 10 | errorMessage: null, 11 | key_path: '', 12 | 13 | save: function(newValue, newTTL) { 14 | var result = true; 15 | if (this.oldValue != newValue) { 16 | var keyObj = this; 17 | params = {value: newValue, prevValue: this.oldValue}; 18 | if (typeof newTTL != "undefined" && newTTL != "") { 19 | params.ttl = newTTL; 20 | } 21 | $.post("/v1/" + this.key_path, params, function() { 22 | this.oldValue = newValue; 23 | }).fail(function(resp) { 24 | keyObj.set('errorMessage', $.parseJSON(resp.responseText)['message']); 25 | result = false; 26 | }); 27 | } 28 | }, 29 | delete: function() { 30 | if (!this.get('deleted')) { 31 | $.ajax({ 32 | type: "DELETE", 33 | url: "/v1/" + this.key_path 34 | }); 35 | this.set('value', 'deleted'); 36 | } 37 | } 38 | }); 39 | 40 | Wetcd.Etcd = Ember.Object.extend({}); 41 | Wetcd.Etcd.reopenClass({ 42 | createKey: function(newKey, newValue, newTTL) { 43 | params = {value: newValue}; 44 | if (typeof newTTL != "undefined" && newTTL != "") { 45 | params.ttl = newTTL; 46 | } 47 | $.post("/v1/keys" + newKey, params); 48 | }, 49 | keys: function(key) { 50 | var path = ''; 51 | if (key != '') { path = key.replace( new RegExp("^\/+"), ''); } 52 | 53 | var list = Em.A(); 54 | 55 | $.get("/v1/" + path, function(response) { 56 | var data = $.parseJSON(response); 57 | if (Ember.isArray(data)) { 58 | data.forEach(function (k) { 59 | k['oldValue'] = k['value']; 60 | k['key_path'] = 'keys'+k['key']; 61 | list.pushObject(Wetcd.Key.create(k)); 62 | }); 63 | } else { 64 | data['oldValue'] = data['value']; 65 | data['key_path'] = 'keys'+data['key']; 66 | list.pushObject(Wetcd.Key.create(data)); 67 | } 68 | }); 69 | 70 | return list; 71 | } 72 | }); 73 | 74 | Wetcd.IndexRoute = Ember.Route.extend({ 75 | redirect: function() { 76 | this.transitionTo('keys', 'keys'); 77 | } 78 | }); 79 | 80 | Wetcd.KeysRoute = Ember.Route.extend({ 81 | newKey: '', 82 | newValue: '', 83 | 84 | model: function(params) { 85 | return Wetcd.Etcd.keys(params.key); 86 | }, 87 | actions: { 88 | createKey: function() { 89 | newKey = this.controller.get('newKey'); 90 | if (newKey[0] != '/') { 91 | newKey = '/'+newKey; 92 | } 93 | Wetcd.Etcd.createKey(newKey, this.controller.get('newValue'), this.controller.get('newTTL')); 94 | 95 | parent = findParent(newKey); 96 | this.controller.set('model', Wetcd.Etcd.keys(parent)); 97 | 98 | $('#add-key').collapse('hide'); 99 | } 100 | } 101 | }); 102 | 103 | Wetcd.KeysController = Ember.ArrayController.extend({ 104 | sortProperties: ['key'], 105 | sortAscending: true 106 | }); 107 | 108 | Wetcd.KeyController = Ember.ObjectController.extend({ 109 | actions: { 110 | edit: function() { 111 | if (!this.get('deleted')) { 112 | this.set('editing', true); 113 | } 114 | }, 115 | cancel: function() { 116 | this.set('editing', false); 117 | }, 118 | update: function() { 119 | if (this.content.save(this.get('value'), this.get('ttl'))) { 120 | this.set('editing', false); 121 | } 122 | }, 123 | delete: function() { 124 | parent = findParent(this.content.key); 125 | this.content.delete(); 126 | this.set('editing', false); 127 | this.set('deleted', true); 128 | this.parentController.set('content', Wetcd.Etcd.keys(parent)); 129 | } 130 | } 131 | }); 132 | 133 | Wetcd.EditValueView = Ember.TextField.extend({ 134 | didInsertElement: function () { 135 | this.$().focus(); 136 | } 137 | }); 138 | 139 | Ember.Handlebars.helper('edit-value', Wetcd.EditValueView); 140 | 141 | // uses moment.js 142 | Ember.Handlebars.helper('format-date', function(date) { 143 | return moment(date).fromNow(); 144 | }); 145 | 146 | var findParent = function (str) { 147 | pieces = str.split('/'); 148 | pieces.pop(); 149 | pieces.unshift('keys') 150 | return pieces.join('/') 151 | } -------------------------------------------------------------------------------- /public/js/libs/handlebars-1.0.0.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (C) 2011 by Yehuda Katz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | // lib/handlebars/browser-prefix.js 26 | var Handlebars = {}; 27 | 28 | (function(Handlebars, undefined) { 29 | ; 30 | // lib/handlebars/base.js 31 | 32 | Handlebars.VERSION = "1.0.0"; 33 | Handlebars.COMPILER_REVISION = 4; 34 | 35 | Handlebars.REVISION_CHANGES = { 36 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 37 | 2: '== 1.0.0-rc.3', 38 | 3: '== 1.0.0-rc.4', 39 | 4: '>= 1.0.0' 40 | }; 41 | 42 | Handlebars.helpers = {}; 43 | Handlebars.partials = {}; 44 | 45 | var toString = Object.prototype.toString, 46 | functionType = '[object Function]', 47 | objectType = '[object Object]'; 48 | 49 | Handlebars.registerHelper = function(name, fn, inverse) { 50 | if (toString.call(name) === objectType) { 51 | if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } 52 | Handlebars.Utils.extend(this.helpers, name); 53 | } else { 54 | if (inverse) { fn.not = inverse; } 55 | this.helpers[name] = fn; 56 | } 57 | }; 58 | 59 | Handlebars.registerPartial = function(name, str) { 60 | if (toString.call(name) === objectType) { 61 | Handlebars.Utils.extend(this.partials, name); 62 | } else { 63 | this.partials[name] = str; 64 | } 65 | }; 66 | 67 | Handlebars.registerHelper('helperMissing', function(arg) { 68 | if(arguments.length === 2) { 69 | return undefined; 70 | } else { 71 | throw new Error("Missing helper: '" + arg + "'"); 72 | } 73 | }); 74 | 75 | Handlebars.registerHelper('blockHelperMissing', function(context, options) { 76 | var inverse = options.inverse || function() {}, fn = options.fn; 77 | 78 | var type = toString.call(context); 79 | 80 | if(type === functionType) { context = context.call(this); } 81 | 82 | if(context === true) { 83 | return fn(this); 84 | } else if(context === false || context == null) { 85 | return inverse(this); 86 | } else if(type === "[object Array]") { 87 | if(context.length > 0) { 88 | return Handlebars.helpers.each(context, options); 89 | } else { 90 | return inverse(this); 91 | } 92 | } else { 93 | return fn(context); 94 | } 95 | }); 96 | 97 | Handlebars.K = function() {}; 98 | 99 | Handlebars.createFrame = Object.create || function(object) { 100 | Handlebars.K.prototype = object; 101 | var obj = new Handlebars.K(); 102 | Handlebars.K.prototype = null; 103 | return obj; 104 | }; 105 | 106 | Handlebars.logger = { 107 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 108 | 109 | methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, 110 | 111 | // can be overridden in the host environment 112 | log: function(level, obj) { 113 | if (Handlebars.logger.level <= level) { 114 | var method = Handlebars.logger.methodMap[level]; 115 | if (typeof console !== 'undefined' && console[method]) { 116 | console[method].call(console, obj); 117 | } 118 | } 119 | } 120 | }; 121 | 122 | Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; 123 | 124 | Handlebars.registerHelper('each', function(context, options) { 125 | var fn = options.fn, inverse = options.inverse; 126 | var i = 0, ret = "", data; 127 | 128 | var type = toString.call(context); 129 | if(type === functionType) { context = context.call(this); } 130 | 131 | if (options.data) { 132 | data = Handlebars.createFrame(options.data); 133 | } 134 | 135 | if(context && typeof context === 'object') { 136 | if(context instanceof Array){ 137 | for(var j = context.length; i 2) { 351 | expected.push("'" + this.terminals_[p] + "'"); 352 | } 353 | if (this.lexer.showPosition) { 354 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; 355 | } else { 356 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 357 | } 358 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 359 | } 360 | } 361 | if (action[0] instanceof Array && action.length > 1) { 362 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 363 | } 364 | switch (action[0]) { 365 | case 1: 366 | stack.push(symbol); 367 | vstack.push(this.lexer.yytext); 368 | lstack.push(this.lexer.yylloc); 369 | stack.push(action[1]); 370 | symbol = null; 371 | if (!preErrorSymbol) { 372 | yyleng = this.lexer.yyleng; 373 | yytext = this.lexer.yytext; 374 | yylineno = this.lexer.yylineno; 375 | yyloc = this.lexer.yylloc; 376 | if (recovering > 0) 377 | recovering--; 378 | } else { 379 | symbol = preErrorSymbol; 380 | preErrorSymbol = null; 381 | } 382 | break; 383 | case 2: 384 | len = this.productions_[action[1]][1]; 385 | yyval.$ = vstack[vstack.length - len]; 386 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 387 | if (ranges) { 388 | yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; 389 | } 390 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 391 | if (typeof r !== "undefined") { 392 | return r; 393 | } 394 | if (len) { 395 | stack = stack.slice(0, -1 * len * 2); 396 | vstack = vstack.slice(0, -1 * len); 397 | lstack = lstack.slice(0, -1 * len); 398 | } 399 | stack.push(this.productions_[action[1]][0]); 400 | vstack.push(yyval.$); 401 | lstack.push(yyval._$); 402 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 403 | stack.push(newState); 404 | break; 405 | case 3: 406 | return true; 407 | } 408 | } 409 | return true; 410 | } 411 | }; 412 | /* Jison generated lexer */ 413 | var lexer = (function(){ 414 | var lexer = ({EOF:1, 415 | parseError:function parseError(str, hash) { 416 | if (this.yy.parser) { 417 | this.yy.parser.parseError(str, hash); 418 | } else { 419 | throw new Error(str); 420 | } 421 | }, 422 | setInput:function (input) { 423 | this._input = input; 424 | this._more = this._less = this.done = false; 425 | this.yylineno = this.yyleng = 0; 426 | this.yytext = this.matched = this.match = ''; 427 | this.conditionStack = ['INITIAL']; 428 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 429 | if (this.options.ranges) this.yylloc.range = [0,0]; 430 | this.offset = 0; 431 | return this; 432 | }, 433 | input:function () { 434 | var ch = this._input[0]; 435 | this.yytext += ch; 436 | this.yyleng++; 437 | this.offset++; 438 | this.match += ch; 439 | this.matched += ch; 440 | var lines = ch.match(/(?:\r\n?|\n).*/g); 441 | if (lines) { 442 | this.yylineno++; 443 | this.yylloc.last_line++; 444 | } else { 445 | this.yylloc.last_column++; 446 | } 447 | if (this.options.ranges) this.yylloc.range[1]++; 448 | 449 | this._input = this._input.slice(1); 450 | return ch; 451 | }, 452 | unput:function (ch) { 453 | var len = ch.length; 454 | var lines = ch.split(/(?:\r\n?|\n)/g); 455 | 456 | this._input = ch + this._input; 457 | this.yytext = this.yytext.substr(0, this.yytext.length-len-1); 458 | //this.yyleng -= len; 459 | this.offset -= len; 460 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 461 | this.match = this.match.substr(0, this.match.length-1); 462 | this.matched = this.matched.substr(0, this.matched.length-1); 463 | 464 | if (lines.length-1) this.yylineno -= lines.length-1; 465 | var r = this.yylloc.range; 466 | 467 | this.yylloc = {first_line: this.yylloc.first_line, 468 | last_line: this.yylineno+1, 469 | first_column: this.yylloc.first_column, 470 | last_column: lines ? 471 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: 472 | this.yylloc.first_column - len 473 | }; 474 | 475 | if (this.options.ranges) { 476 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 477 | } 478 | return this; 479 | }, 480 | more:function () { 481 | this._more = true; 482 | return this; 483 | }, 484 | less:function (n) { 485 | this.unput(this.match.slice(n)); 486 | }, 487 | pastInput:function () { 488 | var past = this.matched.substr(0, this.matched.length - this.match.length); 489 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 490 | }, 491 | upcomingInput:function () { 492 | var next = this.match; 493 | if (next.length < 20) { 494 | next += this._input.substr(0, 20-next.length); 495 | } 496 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 497 | }, 498 | showPosition:function () { 499 | var pre = this.pastInput(); 500 | var c = new Array(pre.length + 1).join("-"); 501 | return pre + this.upcomingInput() + "\n" + c+"^"; 502 | }, 503 | next:function () { 504 | if (this.done) { 505 | return this.EOF; 506 | } 507 | if (!this._input) this.done = true; 508 | 509 | var token, 510 | match, 511 | tempMatch, 512 | index, 513 | col, 514 | lines; 515 | if (!this._more) { 516 | this.yytext = ''; 517 | this.match = ''; 518 | } 519 | var rules = this._currentRules(); 520 | for (var i=0;i < rules.length; i++) { 521 | tempMatch = this._input.match(this.rules[rules[i]]); 522 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 523 | match = tempMatch; 524 | index = i; 525 | if (!this.options.flex) break; 526 | } 527 | } 528 | if (match) { 529 | lines = match[0].match(/(?:\r\n?|\n).*/g); 530 | if (lines) this.yylineno += lines.length; 531 | this.yylloc = {first_line: this.yylloc.last_line, 532 | last_line: this.yylineno+1, 533 | first_column: this.yylloc.last_column, 534 | last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; 535 | this.yytext += match[0]; 536 | this.match += match[0]; 537 | this.matches = match; 538 | this.yyleng = this.yytext.length; 539 | if (this.options.ranges) { 540 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 541 | } 542 | this._more = false; 543 | this._input = this._input.slice(match[0].length); 544 | this.matched += match[0]; 545 | token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); 546 | if (this.done && this._input) this.done = false; 547 | if (token) return token; 548 | else return; 549 | } 550 | if (this._input === "") { 551 | return this.EOF; 552 | } else { 553 | return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 554 | {text: "", token: null, line: this.yylineno}); 555 | } 556 | }, 557 | lex:function lex() { 558 | var r = this.next(); 559 | if (typeof r !== 'undefined') { 560 | return r; 561 | } else { 562 | return this.lex(); 563 | } 564 | }, 565 | begin:function begin(condition) { 566 | this.conditionStack.push(condition); 567 | }, 568 | popState:function popState() { 569 | return this.conditionStack.pop(); 570 | }, 571 | _currentRules:function _currentRules() { 572 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 573 | }, 574 | topState:function () { 575 | return this.conditionStack[this.conditionStack.length-2]; 576 | }, 577 | pushState:function begin(condition) { 578 | this.begin(condition); 579 | }}); 580 | lexer.options = {}; 581 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 582 | 583 | var YYSTATE=YY_START 584 | switch($avoiding_name_collisions) { 585 | case 0: yy_.yytext = "\\"; return 14; 586 | break; 587 | case 1: 588 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 589 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 590 | if(yy_.yytext) return 14; 591 | 592 | break; 593 | case 2: return 14; 594 | break; 595 | case 3: 596 | if(yy_.yytext.slice(-1) !== "\\") this.popState(); 597 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); 598 | return 14; 599 | 600 | break; 601 | case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; 602 | break; 603 | case 5: return 25; 604 | break; 605 | case 6: return 16; 606 | break; 607 | case 7: return 20; 608 | break; 609 | case 8: return 19; 610 | break; 611 | case 9: return 19; 612 | break; 613 | case 10: return 23; 614 | break; 615 | case 11: return 22; 616 | break; 617 | case 12: this.popState(); this.begin('com'); 618 | break; 619 | case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 620 | break; 621 | case 14: return 22; 622 | break; 623 | case 15: return 37; 624 | break; 625 | case 16: return 36; 626 | break; 627 | case 17: return 36; 628 | break; 629 | case 18: return 40; 630 | break; 631 | case 19: /*ignore whitespace*/ 632 | break; 633 | case 20: this.popState(); return 24; 634 | break; 635 | case 21: this.popState(); return 18; 636 | break; 637 | case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31; 638 | break; 639 | case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31; 640 | break; 641 | case 24: return 38; 642 | break; 643 | case 25: return 33; 644 | break; 645 | case 26: return 33; 646 | break; 647 | case 27: return 32; 648 | break; 649 | case 28: return 36; 650 | break; 651 | case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36; 652 | break; 653 | case 30: return 'INVALID'; 654 | break; 655 | case 31: return 5; 656 | break; 657 | } 658 | }; 659 | lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; 660 | lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}}; 661 | return lexer;})() 662 | parser.lexer = lexer; 663 | function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; 664 | return new Parser; 665 | })();; 666 | // lib/handlebars/compiler/base.js 667 | 668 | Handlebars.Parser = handlebars; 669 | 670 | Handlebars.parse = function(input) { 671 | 672 | // Just return if an already-compile AST was passed in. 673 | if(input.constructor === Handlebars.AST.ProgramNode) { return input; } 674 | 675 | Handlebars.Parser.yy = Handlebars.AST; 676 | return Handlebars.Parser.parse(input); 677 | }; 678 | ; 679 | // lib/handlebars/compiler/ast.js 680 | Handlebars.AST = {}; 681 | 682 | Handlebars.AST.ProgramNode = function(statements, inverse) { 683 | this.type = "program"; 684 | this.statements = statements; 685 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 686 | }; 687 | 688 | Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { 689 | this.type = "mustache"; 690 | this.escaped = !unescaped; 691 | this.hash = hash; 692 | 693 | var id = this.id = rawParams[0]; 694 | var params = this.params = rawParams.slice(1); 695 | 696 | // a mustache is an eligible helper if: 697 | // * its id is simple (a single part, not `this` or `..`) 698 | var eligibleHelper = this.eligibleHelper = id.isSimple; 699 | 700 | // a mustache is definitely a helper if: 701 | // * it is an eligible helper, and 702 | // * it has at least one parameter or hash segment 703 | this.isHelper = eligibleHelper && (params.length || hash); 704 | 705 | // if a mustache is an eligible helper but not a definite 706 | // helper, it is ambiguous, and will be resolved in a later 707 | // pass or at runtime. 708 | }; 709 | 710 | Handlebars.AST.PartialNode = function(partialName, context) { 711 | this.type = "partial"; 712 | this.partialName = partialName; 713 | this.context = context; 714 | }; 715 | 716 | Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { 717 | var verifyMatch = function(open, close) { 718 | if(open.original !== close.original) { 719 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 720 | } 721 | }; 722 | 723 | verifyMatch(mustache.id, close); 724 | this.type = "block"; 725 | this.mustache = mustache; 726 | this.program = program; 727 | this.inverse = inverse; 728 | 729 | if (this.inverse && !this.program) { 730 | this.isInverse = true; 731 | } 732 | }; 733 | 734 | Handlebars.AST.ContentNode = function(string) { 735 | this.type = "content"; 736 | this.string = string; 737 | }; 738 | 739 | Handlebars.AST.HashNode = function(pairs) { 740 | this.type = "hash"; 741 | this.pairs = pairs; 742 | }; 743 | 744 | Handlebars.AST.IdNode = function(parts) { 745 | this.type = "ID"; 746 | 747 | var original = "", 748 | dig = [], 749 | depth = 0; 750 | 751 | for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + original); } 757 | else if (part === "..") { depth++; } 758 | else { this.isScoped = true; } 759 | } 760 | else { dig.push(part); } 761 | } 762 | 763 | this.original = original; 764 | this.parts = dig; 765 | this.string = dig.join('.'); 766 | this.depth = depth; 767 | 768 | // an ID is simple if it only has one part, and that part is not 769 | // `..` or `this`. 770 | this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; 771 | 772 | this.stringModeValue = this.string; 773 | }; 774 | 775 | Handlebars.AST.PartialNameNode = function(name) { 776 | this.type = "PARTIAL_NAME"; 777 | this.name = name.original; 778 | }; 779 | 780 | Handlebars.AST.DataNode = function(id) { 781 | this.type = "DATA"; 782 | this.id = id; 783 | }; 784 | 785 | Handlebars.AST.StringNode = function(string) { 786 | this.type = "STRING"; 787 | this.original = 788 | this.string = 789 | this.stringModeValue = string; 790 | }; 791 | 792 | Handlebars.AST.IntegerNode = function(integer) { 793 | this.type = "INTEGER"; 794 | this.original = 795 | this.integer = integer; 796 | this.stringModeValue = Number(integer); 797 | }; 798 | 799 | Handlebars.AST.BooleanNode = function(bool) { 800 | this.type = "BOOLEAN"; 801 | this.bool = bool; 802 | this.stringModeValue = bool === "true"; 803 | }; 804 | 805 | Handlebars.AST.CommentNode = function(comment) { 806 | this.type = "comment"; 807 | this.comment = comment; 808 | }; 809 | ; 810 | // lib/handlebars/utils.js 811 | 812 | var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 813 | 814 | Handlebars.Exception = function(message) { 815 | var tmp = Error.prototype.constructor.apply(this, arguments); 816 | 817 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 818 | for (var idx = 0; idx < errorProps.length; idx++) { 819 | this[errorProps[idx]] = tmp[errorProps[idx]]; 820 | } 821 | }; 822 | Handlebars.Exception.prototype = new Error(); 823 | 824 | // Build out our basic SafeString type 825 | Handlebars.SafeString = function(string) { 826 | this.string = string; 827 | }; 828 | Handlebars.SafeString.prototype.toString = function() { 829 | return this.string.toString(); 830 | }; 831 | 832 | var escape = { 833 | "&": "&", 834 | "<": "<", 835 | ">": ">", 836 | '"': """, 837 | "'": "'", 838 | "`": "`" 839 | }; 840 | 841 | var badChars = /[&<>"'`]/g; 842 | var possible = /[&<>"'`]/; 843 | 844 | var escapeChar = function(chr) { 845 | return escape[chr] || "&"; 846 | }; 847 | 848 | Handlebars.Utils = { 849 | extend: function(obj, value) { 850 | for(var key in value) { 851 | if(value.hasOwnProperty(key)) { 852 | obj[key] = value[key]; 853 | } 854 | } 855 | }, 856 | 857 | escapeExpression: function(string) { 858 | // don't escape SafeStrings, since they're already safe 859 | if (string instanceof Handlebars.SafeString) { 860 | return string.toString(); 861 | } else if (string == null || string === false) { 862 | return ""; 863 | } 864 | 865 | // Force a string conversion as this will be done by the append regardless and 866 | // the regex test will do this transparently behind the scenes, causing issues if 867 | // an object's to string has escaped characters in it. 868 | string = string.toString(); 869 | 870 | if(!possible.test(string)) { return string; } 871 | return string.replace(badChars, escapeChar); 872 | }, 873 | 874 | isEmpty: function(value) { 875 | if (!value && value !== 0) { 876 | return true; 877 | } else if(toString.call(value) === "[object Array]" && value.length === 0) { 878 | return true; 879 | } else { 880 | return false; 881 | } 882 | } 883 | }; 884 | ; 885 | // lib/handlebars/compiler/compiler.js 886 | 887 | /*jshint eqnull:true*/ 888 | var Compiler = Handlebars.Compiler = function() {}; 889 | var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {}; 890 | 891 | // the foundHelper register will disambiguate helper lookup from finding a 892 | // function in a context. This is necessary for mustache compatibility, which 893 | // requires that context functions in blocks are evaluated by blockHelperMissing, 894 | // and then proceed as if the resulting value was provided to blockHelperMissing. 895 | 896 | Compiler.prototype = { 897 | compiler: Compiler, 898 | 899 | disassemble: function() { 900 | var opcodes = this.opcodes, opcode, out = [], params, param; 901 | 902 | for (var i=0, l=opcodes.length; i 0) { 1414 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1415 | } 1416 | 1417 | // Generate minimizer alias mappings 1418 | if (!this.isChild) { 1419 | for (var alias in this.context.aliases) { 1420 | if (this.context.aliases.hasOwnProperty(alias)) { 1421 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1422 | } 1423 | } 1424 | } 1425 | 1426 | if (this.source[1]) { 1427 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1428 | } 1429 | 1430 | // Merge children 1431 | if (!this.isChild) { 1432 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1433 | } 1434 | 1435 | if (!this.environment.isSimple) { 1436 | this.source.push("return buffer;"); 1437 | } 1438 | 1439 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1440 | 1441 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1975 | return this.topStackName(); 1976 | }, 1977 | topStackName: function() { 1978 | return "stack" + this.stackSlot; 1979 | }, 1980 | flushInline: function() { 1981 | var inlineStack = this.inlineStack; 1982 | if (inlineStack.length) { 1983 | this.inlineStack = []; 1984 | for (var i = 0, len = inlineStack.length; i < len; i++) { 1985 | var entry = inlineStack[i]; 1986 | if (entry instanceof Literal) { 1987 | this.compileStack.push(entry); 1988 | } else { 1989 | this.pushStack(entry); 1990 | } 1991 | } 1992 | } 1993 | }, 1994 | isInline: function() { 1995 | return this.inlineStack.length; 1996 | }, 1997 | 1998 | popStack: function(wrapped) { 1999 | var inline = this.isInline(), 2000 | item = (inline ? this.inlineStack : this.compileStack).pop(); 2001 | 2002 | if (!wrapped && (item instanceof Literal)) { 2003 | return item.value; 2004 | } else { 2005 | if (!inline) { 2006 | this.stackSlot--; 2007 | } 2008 | return item; 2009 | } 2010 | }, 2011 | 2012 | topStack: function(wrapped) { 2013 | var stack = (this.isInline() ? this.inlineStack : this.compileStack), 2014 | item = stack[stack.length - 1]; 2015 | 2016 | if (!wrapped && (item instanceof Literal)) { 2017 | return item.value; 2018 | } else { 2019 | return item; 2020 | } 2021 | }, 2022 | 2023 | quotedString: function(str) { 2024 | return '"' + str 2025 | .replace(/\\/g, '\\\\') 2026 | .replace(/"/g, '\\"') 2027 | .replace(/\n/g, '\\n') 2028 | .replace(/\r/g, '\\r') 2029 | .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 2030 | .replace(/\u2029/g, '\\u2029') + '"'; 2031 | }, 2032 | 2033 | setupHelper: function(paramSize, name, missingParams) { 2034 | var params = []; 2035 | this.setupParams(paramSize, params, missingParams); 2036 | var foundHelper = this.nameLookup('helpers', name, 'helper'); 2037 | 2038 | return { 2039 | params: params, 2040 | name: foundHelper, 2041 | callParams: ["depth0"].concat(params).join(", "), 2042 | helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") 2043 | }; 2044 | }, 2045 | 2046 | // the params and contexts arguments are passed in arrays 2047 | // to fill in 2048 | setupParams: function(paramSize, params, useRegister) { 2049 | var options = [], contexts = [], types = [], param, inverse, program; 2050 | 2051 | options.push("hash:" + this.popStack()); 2052 | 2053 | inverse = this.popStack(); 2054 | program = this.popStack(); 2055 | 2056 | // Avoid setting fn and inverse if neither are set. This allows 2057 | // helpers to do a check for `if (options.fn)` 2058 | if (program || inverse) { 2059 | if (!program) { 2060 | this.context.aliases.self = "this"; 2061 | program = "self.noop"; 2062 | } 2063 | 2064 | if (!inverse) { 2065 | this.context.aliases.self = "this"; 2066 | inverse = "self.noop"; 2067 | } 2068 | 2069 | options.push("inverse:" + inverse); 2070 | options.push("fn:" + program); 2071 | } 2072 | 2073 | for(var i=0; i