├── .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