From 33526ed4d9ef4d62efebbfae4ed1f44e1c82a2a5 Mon Sep 17 00:00:00 2001 From: David Schoonover Date: Tue, 17 Jul 2012 16:08:24 -0700 Subject: [PATCH] Applicaton now obeys a mount-point set by middleware. --- Cokefile | 4 +- README.md | 57 ---------- lib/base/base-model.js | 12 +- lib/base/base-model.mod.js | 12 +- lib/base/base.js | 11 +- lib/base/base.mod.js | 11 +- lib/base/index.js | 4 +- lib/base/index.mod.js | 4 +- lib/base/parser-mixin.js | 100 +++++++++++++++++ lib/base/parser-mixin.mod.js | 104 +++++++++++++++++ lib/chart/chart-type.js | 10 +- lib/chart/chart-type.mod.js | 10 +- lib/chart/option/chart-option-model.js | 5 +- lib/chart/option/chart-option-model.mod.js | 5 +- lib/chart/type/d3-chart.js | 6 +- lib/chart/type/d3-chart.mod.js | 6 +- lib/chart/type/dygraphs.js | 4 +- lib/chart/type/dygraphs.mod.js | 4 +- lib/client.js | 162 +++++++++++++++++++++++++++ lib/client.mod.js | 166 ++++++++++++++++++++++++++++ lib/data/datasource-model.js | 11 +- lib/data/datasource-model.mod.js | 11 +- lib/graph/graph-model.js | 9 +- lib/graph/graph-model.mod.js | 9 +- lib/limn.js | 18 +++- lib/limn.mod.js | 18 +++- lib/server/middleware.js | 13 ++- lib/server/server.js | 5 +- lib/util/index.js | 5 +- lib/util/index.mod.js | 5 +- lib/util/parser.js | 101 +----------------- lib/util/parser.mod.js | 99 +---------------- src/base/base-model.co | 8 +- src/base/base.co | 7 +- src/base/index.co | 3 + src/base/parser-mixin.co | 86 ++++++++++++++ src/chart/chart-type.co | 10 +- src/chart/option/chart-option-model.co | 6 +- src/chart/type/d3-chart.co | 6 +- src/chart/type/dygraphs.co | 4 +- src/client.co | 161 +++++++++++++++++++++++++++ src/data/datasource-model.co | 8 +- src/graph/graph-model.co | 7 +- src/limn.co | 149 ------------------------- src/server/index.js | 1 - src/server/middleware.co | 9 +- src/server/server.co | 5 +- src/util/index.co | 3 +- src/util/parser.co | 82 -------------- src/version.js | 2 +- www/css/hicons.css | 4 +- www/css/hicons.styl | 4 +- www/css/layout.css | 6 +- www/css/layout.styl | 6 +- www/footer.jade | 87 +++++++++++++++ www/geo.jade | 2 +- www/layout.jade | 94 +--------------- www/mixins/helpers.jade | 11 +- www/modules.yaml | 3 +- 59 files changed, 1074 insertions(+), 701 deletions(-) create mode 100644 lib/base/parser-mixin.js create mode 100644 lib/base/parser-mixin.mod.js create mode 100644 lib/client.js create mode 100644 lib/client.mod.js create mode 100644 src/base/parser-mixin.co create mode 100644 src/client.co delete mode 100644 src/limn.co create mode 100644 www/footer.jade diff --git a/Cokefile b/Cokefile index c19c035..4f39c17 100644 --- a/Cokefile +++ b/Cokefile @@ -35,9 +35,7 @@ task \setup 'Ensure project is set up for development.' -> task \server 'Start dev server' -> - invoke \setup - say '' - run 'src/server/server.co' + run './src/server/server.co' task \build 'Build coco sources' -> diff --git a/README.md b/README.md index befe80d..2bfa087 100644 --- a/README.md +++ b/README.md @@ -2,60 +2,3 @@ -### Setting Up - -This is a [node.js][nodejs] project, so you need node > 0.6.x and the node package manager, [npm][npm]. - -Once that's done, I recommend you install Coco globally so you don't have to futz with your path: `npm install -g coco` -- if you choose not to, you should probably add `./node_modules/.bin` to `PATH` in your bashrc or something. - -Next, install your source checkout in dev mode by running `npm link` from the project root. This will also download and install all the package dependencies. - -Finally, start the `server` task using Coke (it's like Rake for Coco) with `coke server`. While this does what it says on the tin, it also seems to have a habit of randomly losing parts of stderr. For now, you can work around this by manually starting the server: `coke link && lib/server/server.co` - -You should now have a server running on 8081. - -### Project Layout -- assets/ - static images -- data/ - json, yaml, csv, etc. files that contain graph configuration and graph content data -- data/datasources/ - graph content data -- data/graphs/ - saved graph configurations. -- lib/ - [Coco][coco] files. Application logic lives here. -- lib/{chart,dashboard,dataset,graph}/ - Models and View Classes -- lib/template/ - client side [Jade][jade] views. These are included and rendered by View classes. -- lib/server/ - Server side [Coco][coco] files. -- lib/server/server.co - [Express][expressjs] server setup. Routing is done here. -- lib/server/controllers/ - Server side controllers. Routed to by [express-resource][]. -- www/ - (Mostly) static [Jade][jade] HTML templates and [Stylus][stylus] CSS templates. The [Jade][jade] templates are rendered by the server side controllers in lib/server/controllers/. -- var/ - Compiled JavaScript and CSS files. - -### Deployment -Coco needs to be compiled down to JavaScript in order for it to be executed. In development environments, this is done on the fly. In production environments, all Coco is compiled down into JavaScript files and placed in a dist/ directory. These JavaScript (and compiled Stylus CSS files) are served up directly to the browser upon request, rather than having to be compiled first. - -deploy.sh currently builds a distribution tmp/dist, and then rsyncs this over to reportcard.wmflabs.org. You will need an account with sudo permissions on reportcard2.pmtpa.wmflabs in order to deploy. - - -### Notes - -- This project is written in [Coco][coco], a dialect of [CoffeeScript][coffee] -- they both compile - down to JavaScript. The pair are very, very similar, [except][coco-improvements] - for [a few][coco-incompatibilities] [things][coco-vs-coffee]. If you can read JavaScript and Ruby, - you can understand Coco and CoffeeScript. (I refer to the [CoffeeScript docs][coffee-docs] for - the syntax, and I find the [comparison page][coco-vs-coffee] to be the best reference for Coco.) - -- Coco require compilation before it'll run in the browser (though node can run it directly -- `#!/usr/bin/env coco` will work as a shebang as well). I've written [request middleware][connect-compiler] that recompiles stale files on demand, and it is pretty cool. - - - -[nodejs]: http://nodejs.org/ -[npm]: http://npmjs.org/ -[coco]: https://github.com/satyr/coco -[coco-vs-coffee]: https://github.com/satyr/coco/wiki/side-by-side-comparison -[coco-improvements]: https://github.com/satyr/coco/wiki/improvements -[coco-incompatibilities]: https://github.com/satyr/coco/wiki/incompatibilities -[coffee]: http://coffeescript.org/ -[coffee-docs]: http://coffeescript.org/#language -[connect-compiler]: https://github.com/dsc/connect-compiler -[jade]: https://github.com/visionmedia/jade -[expressjs]: http://expressjs.com/guide.html -[express-resource]: https://github.com/visionmedia/express-resource -[stylus]: http://learnboost.github.com/stylus/ \ No newline at end of file diff --git a/lib/base/base-model.js b/lib/base/base-model.js index 2e3394c..7fe3c06 100644 --- a/lib/base/base-model.js +++ b/lib/base/base-model.js @@ -1,5 +1,6 @@ -var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +var Backbone, limn, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; Backbone = require('backbone'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; _ref = require('./base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; /** @@ -17,7 +18,7 @@ BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ return BaseModel; }()), url: function(){ - return this.urlRoot + "/" + this.get('id') + ".json"; + return limn.mount(this.urlRoot) + "/" + this.get('id') + ".json"; }, has: function(key){ return this.get(key) != null; @@ -221,12 +222,13 @@ BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ }); }, url: function(){ - var id; + var root, id; + root = limn.mount(this.urlRoot); id = this.get('id') || this.get('slug'); if (id) { - return this.urlRoot + "/" + id + ".json"; + return root + "/" + id + ".json"; } else { - return this.urlRoot + ".json"; + return root + ".json"; } }, load: function(){ diff --git a/lib/base/base-model.mod.js b/lib/base/base-model.mod.js index 9d7f227..421525a 100644 --- a/lib/base/base-model.mod.js +++ b/lib/base/base-model.mod.js @@ -1,7 +1,8 @@ require.define('/node_modules/limn/base/base-model.js', function(require, module, exports, __dirname, __filename, undefined){ -var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +var Backbone, limn, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; Backbone = require('backbone'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; _ref = require('./base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; /** @@ -19,7 +20,7 @@ BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ return BaseModel; }()), url: function(){ - return this.urlRoot + "/" + this.get('id') + ".json"; + return limn.mount(this.urlRoot) + "/" + this.get('id') + ".json"; }, has: function(key){ return this.get(key) != null; @@ -223,12 +224,13 @@ BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ }); }, url: function(){ - var id; + var root, id; + root = limn.mount(this.urlRoot); id = this.get('id') || this.get('slug'); if (id) { - return this.urlRoot + "/" + id + ".json"; + return root + "/" + id + ".json"; } else { - return this.urlRoot + ".json"; + return root + ".json"; } }, load: function(){ diff --git a/lib/base/base.js b/lib/base/base.js index eac2083..701c9cc 100644 --- a/lib/base/base.js +++ b/lib/base/base.js @@ -18,11 +18,14 @@ Base = (function(superclass){ Base.displayName = 'Base'; var prototype = __extend(Base, superclass).prototype, constructor = Base; function Base(){ + var _ref; this.__class__ = this.constructor; this.__superclass__ = this.constructor.superclass; this.__apply_bind__(); superclass.call(this); - this.__class__.emit('new', this); + if (typeof (_ref = this.__class__).emit == 'function') { + _ref.emit('new', this); + } } /** * A list of method-names to bind on `initialize`; set this on a subclass to override. @@ -46,10 +49,10 @@ Base = (function(superclass){ return this.getClassName() + "()"; }; Base.extended = function(Subclass){ - var k, v, _own = {}.hasOwnProperty; - for (k in this) if (_own.call(this, k)) { + var k, v; + for (k in this) { v = this[k]; - if (typeof v === 'function') { + if (typeof v === 'function' && !_.contains(['apply', 'call', 'constructor', 'toString'], k)) { Subclass[k] = v; } } diff --git a/lib/base/base.mod.js b/lib/base/base.mod.js index d8daec5..77c5599 100644 --- a/lib/base/base.mod.js +++ b/lib/base/base.mod.js @@ -20,11 +20,14 @@ Base = (function(superclass){ Base.displayName = 'Base'; var prototype = __extend(Base, superclass).prototype, constructor = Base; function Base(){ + var _ref; this.__class__ = this.constructor; this.__superclass__ = this.constructor.superclass; this.__apply_bind__(); superclass.call(this); - this.__class__.emit('new', this); + if (typeof (_ref = this.__class__).emit == 'function') { + _ref.emit('new', this); + } } /** * A list of method-names to bind on `initialize`; set this on a subclass to override. @@ -48,10 +51,10 @@ Base = (function(superclass){ return this.getClassName() + "()"; }; Base.extended = function(Subclass){ - var k, v, _own = {}.hasOwnProperty; - for (k in this) if (_own.call(this, k)) { + var k, v; + for (k in this) { v = this[k]; - if (typeof v === 'function') { + if (typeof v === 'function' && !_.contains(['apply', 'call', 'constructor', 'toString'], k)) { Subclass[k] = v; } } diff --git a/lib/base/index.js b/lib/base/index.js index 5caef36..4334860 100644 --- a/lib/base/index.js +++ b/lib/base/index.js @@ -1,4 +1,4 @@ -var mixins, models, views, cache, cascading, data_binding; +var mixins, models, views, cache, cascading, data_binding, parser_mixin; exports.Base = require('./base'); mixins = require('./base-mixin'); models = require('./base-model'); @@ -7,6 +7,8 @@ cache = require('./model-cache'); cascading = require('./cascading-model'); data_binding = require('./data-binding'); __import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +parser_mixin = require('./parser-mixin'); +__import(exports, parser_mixin); function __import(obj, src){ var own = {}.hasOwnProperty; for (var key in src) if (own.call(src, key)) obj[key] = src[key]; diff --git a/lib/base/index.mod.js b/lib/base/index.mod.js index d23b254..f7867c9 100644 --- a/lib/base/index.mod.js +++ b/lib/base/index.mod.js @@ -1,6 +1,6 @@ require.define('/node_modules/limn/base/index.js', function(require, module, exports, __dirname, __filename, undefined){ -var mixins, models, views, cache, cascading, data_binding; +var mixins, models, views, cache, cascading, data_binding, parser_mixin; exports.Base = require('./base'); mixins = require('./base-mixin'); models = require('./base-model'); @@ -9,6 +9,8 @@ cache = require('./model-cache'); cascading = require('./cascading-model'); data_binding = require('./data-binding'); __import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +parser_mixin = require('./parser-mixin'); +__import(exports, parser_mixin); function __import(obj, src){ var own = {}.hasOwnProperty; for (var key in src) if (own.call(src, key)) obj[key] = src[key]; diff --git a/lib/base/parser-mixin.js b/lib/base/parser-mixin.js new file mode 100644 index 0000000..8a67ce4 --- /dev/null +++ b/lib/base/parser-mixin.js @@ -0,0 +1,100 @@ +var Parsers, BaseModel, BaseList, BaseView, Mixin, ParserMixin, ParsingModel, ParsingList, ParsingView, _ref; +Parsers = require('../util/parser').Parsers; +_ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin; +exports.Parsers = Parsers; +/** + * @class Methods for a class to select parsers by type reflection. + * @mixin + */ +exports.ParserMixin = ParserMixin = (function(superclass){ + ParserMixin.displayName = 'ParserMixin'; + var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin; + __import(ParserMixin.prototype, Parsers); + function ParserMixin(target){ + return Mixin.call(ParserMixin, target); + } + prototype.parseValue = function(v, type){ + return this.getParser(type)(v); + }; + prototype.getParser = function(type){ + var fn, t, _i, _ref, _len; + type == null && (type = 'String'); + fn = this["parse" + type]; + if (typeof fn === 'function') { + return fn; + } + type = _(String(type).toLowerCase()); + for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) { + t = _ref[_i]; + if (type.startsWith(t.toLowerCase())) { + return this["parse" + t]; + } + } + return this.defaultParser || this.parseString; + }; + prototype.getParserFromExample = function(v){ + var type; + if (v == null) { + return null; + } + type = typeof v; + if (type !== 'object') { + return this.getParser(type); + } else if (_.isArray(v)) { + return this.getParser('Array'); + } else { + return this.getParser('Object'); + } + }; + return ParserMixin; +}(Mixin)); +/** + * @class Basic model which mixes in the ParserMixin. + * @extends BaseModel + * @borrows ParserMixin + */ +ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingModel(){ + return BaseModel.apply(this, arguments); + } + return ParsingModel; + }()) +})); +/** + * @class Basic collection which mixes in the ParserMixin. + * @extends BaseList + * @borrows ParserMixin + */ +ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingList(){ + return BaseList.apply(this, arguments); + } + return ParsingList; + }()) +})); +/** + * @class Basic view which mixes in the ParserMixin. + * @extends BaseView + * @borrows ParserMixin + */ +ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingView(){ + return BaseView.apply(this, arguments); + } + return ParsingView; + }()) +})); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/parser-mixin.mod.js b/lib/base/parser-mixin.mod.js new file mode 100644 index 0000000..510e807 --- /dev/null +++ b/lib/base/parser-mixin.mod.js @@ -0,0 +1,104 @@ +require.define('/node_modules/limn/base/parser-mixin.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Parsers, BaseModel, BaseList, BaseView, Mixin, ParserMixin, ParsingModel, ParsingList, ParsingView, _ref; +Parsers = require('../util/parser').Parsers; +_ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin; +exports.Parsers = Parsers; +/** + * @class Methods for a class to select parsers by type reflection. + * @mixin + */ +exports.ParserMixin = ParserMixin = (function(superclass){ + ParserMixin.displayName = 'ParserMixin'; + var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin; + __import(ParserMixin.prototype, Parsers); + function ParserMixin(target){ + return Mixin.call(ParserMixin, target); + } + prototype.parseValue = function(v, type){ + return this.getParser(type)(v); + }; + prototype.getParser = function(type){ + var fn, t, _i, _ref, _len; + type == null && (type = 'String'); + fn = this["parse" + type]; + if (typeof fn === 'function') { + return fn; + } + type = _(String(type).toLowerCase()); + for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) { + t = _ref[_i]; + if (type.startsWith(t.toLowerCase())) { + return this["parse" + t]; + } + } + return this.defaultParser || this.parseString; + }; + prototype.getParserFromExample = function(v){ + var type; + if (v == null) { + return null; + } + type = typeof v; + if (type !== 'object') { + return this.getParser(type); + } else if (_.isArray(v)) { + return this.getParser('Array'); + } else { + return this.getParser('Object'); + } + }; + return ParserMixin; +}(Mixin)); +/** + * @class Basic model which mixes in the ParserMixin. + * @extends BaseModel + * @borrows ParserMixin + */ +ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingModel(){ + return BaseModel.apply(this, arguments); + } + return ParsingModel; + }()) +})); +/** + * @class Basic collection which mixes in the ParserMixin. + * @extends BaseList + * @borrows ParserMixin + */ +ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingList(){ + return BaseList.apply(this, arguments); + } + return ParsingList; + }()) +})); +/** + * @class Basic view which mixes in the ParserMixin. + * @extends BaseView + * @borrows ParserMixin + */ +ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({ + constructor: (function(){ + function ParsingView(){ + return BaseView.apply(this, arguments); + } + return ParsingView; + }()) +})); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/chart-type.js b/lib/chart/chart-type.js index 721046d..c7aad90 100644 --- a/lib/chart/chart-type.js +++ b/lib/chart/chart-type.js @@ -1,9 +1,10 @@ -var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +var moment, Backbone, limn, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; moment = require('moment'); Backbone = require('backbone'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; ReadyEmitter = require('../util/event').ReadyEmitter; -_ref = require('../util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +_ref = require('../base'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; /** * Map of known libraries by name. * @type Object @@ -140,13 +141,14 @@ exports.ChartType = ChartType = (function(superclass){ * info about valid options, along with their types and defaults. */ prototype.loadSpec = function(){ - var proto, _this = this; + var proto, url, _this = this; if (this.ready) { return this; } proto = this.constructor.prototype; + url = (limn.config.mount !== '/' ? limn.config.mount : '') + this.SPEC_URL; jQuery.ajax({ - url: this.SPEC_URL, + url: url, dataType: 'json', success: function(spec){ proto.spec = spec; diff --git a/lib/chart/chart-type.mod.js b/lib/chart/chart-type.mod.js index e62af90..6362824 100644 --- a/lib/chart/chart-type.mod.js +++ b/lib/chart/chart-type.mod.js @@ -1,11 +1,12 @@ require.define('/node_modules/limn/chart/chart-type.js', function(require, module, exports, __dirname, __filename, undefined){ -var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +var moment, Backbone, limn, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; moment = require('moment'); Backbone = require('backbone'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; ReadyEmitter = require('../util/event').ReadyEmitter; -_ref = require('../util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +_ref = require('../base'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; /** * Map of known libraries by name. * @type Object @@ -142,13 +143,14 @@ exports.ChartType = ChartType = (function(superclass){ * info about valid options, along with their types and defaults. */ prototype.loadSpec = function(){ - var proto, _this = this; + var proto, url, _this = this; if (this.ready) { return this; } proto = this.constructor.prototype; + url = (limn.config.mount !== '/' ? limn.config.mount : '') + this.SPEC_URL; jQuery.ajax({ - url: this.SPEC_URL, + url: url, dataType: 'json', success: function(spec){ proto.spec = spec; diff --git a/lib/chart/option/chart-option-model.js b/lib/chart/option/chart-option-model.js index f066113..b6b58c0 100644 --- a/lib/chart/option/chart-option-model.js +++ b/lib/chart/option/chart-option-model.js @@ -1,7 +1,6 @@ -var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +var op, BaseModel, BaseList, Parsers, ParserMixin, ParsingModel, ParsingView, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; _ref = require('../../util'), _ = _ref._, op = _ref.op; -_ref = require('../../util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; -_ref = require('../../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('../../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; /** * @class A set of tags. */ diff --git a/lib/chart/option/chart-option-model.mod.js b/lib/chart/option/chart-option-model.mod.js index 0e5a921..5214160 100644 --- a/lib/chart/option/chart-option-model.mod.js +++ b/lib/chart/option/chart-option-model.mod.js @@ -1,9 +1,8 @@ require.define('/node_modules/limn/chart/option/chart-option-model.js', function(require, module, exports, __dirname, __filename, undefined){ -var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +var op, BaseModel, BaseList, Parsers, ParserMixin, ParsingModel, ParsingView, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; _ref = require('../../util'), _ = _ref._, op = _ref.op; -_ref = require('../../util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; -_ref = require('../../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('../../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; /** * @class A set of tags. */ diff --git a/lib/chart/type/d3-chart.js b/lib/chart/type/d3-chart.js index d5a5dbd..80678c5 100644 --- a/lib/chart/type/d3-chart.js +++ b/lib/chart/type/d3-chart.js @@ -1,9 +1,9 @@ var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; d3 = require('d3'); ColorBrewer = require('colorbrewer'); -_ref = require('../../../util'), _ = _ref._, op = _ref.op; -ChartType = require('../../chart-type').ChartType; -D3ChartElement = require('./d3-chart-element').D3ChartElement; +_ref = require('../../util'), _ = _ref._, op = _ref.op; +ChartType = require('../chart-type').ChartType; +D3ChartElement = require('./d3/d3-chart-element').D3ChartElement; root = function(){ return this; }(); diff --git a/lib/chart/type/d3-chart.mod.js b/lib/chart/type/d3-chart.mod.js index a84a2d3..a4ee3cc 100644 --- a/lib/chart/type/d3-chart.mod.js +++ b/lib/chart/type/d3-chart.mod.js @@ -3,9 +3,9 @@ require.define('/node_modules/limn/chart/type/d3-chart.js', function(require, mo var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; d3 = require('d3'); ColorBrewer = require('colorbrewer'); -_ref = require('../../../util'), _ = _ref._, op = _ref.op; -ChartType = require('../../chart-type').ChartType; -D3ChartElement = require('./d3-chart-element').D3ChartElement; +_ref = require('../../util'), _ = _ref._, op = _ref.op; +ChartType = require('../chart-type').ChartType; +D3ChartElement = require('./d3/d3-chart-element').D3ChartElement; root = function(){ return this; }(); diff --git a/lib/chart/type/dygraphs.js b/lib/chart/type/dygraphs.js index b5228d4..decf44e 100644 --- a/lib/chart/type/dygraphs.js +++ b/lib/chart/type/dygraphs.js @@ -1,6 +1,6 @@ var ChartType, DygraphsChartType, _; -_ = require('../../../util/underscore'); -ChartType = require('../../chart-type').ChartType; +_ = require('../../util/underscore'); +ChartType = require('../chart-type').ChartType; exports.DygraphsChartType = DygraphsChartType = (function(superclass){ DygraphsChartType.displayName = 'DygraphsChartType'; var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; diff --git a/lib/chart/type/dygraphs.mod.js b/lib/chart/type/dygraphs.mod.js index 44f190a..1f01660 100644 --- a/lib/chart/type/dygraphs.mod.js +++ b/lib/chart/type/dygraphs.mod.js @@ -1,8 +1,8 @@ require.define('/node_modules/limn/chart/type/dygraphs.js', function(require, module, exports, __dirname, __filename, undefined){ var ChartType, DygraphsChartType, _; -_ = require('../../../util/underscore'); -ChartType = require('../../chart-type').ChartType; +_ = require('../../util/underscore'); +ChartType = require('../chart-type').ChartType; exports.DygraphsChartType = DygraphsChartType = (function(superclass){ DygraphsChartType.displayName = 'DygraphsChartType'; var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..ceb7666 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,162 @@ +var EventEmitter, op, event, root, limn, emitter, k, Backbone, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _, _i, _len; +EventEmitter = require('events').EventEmitter; +_ref = require('./util'), _ = _ref._, op = _ref.op, event = _ref.event, root = _ref.root; +limn = exports; +emitter = limn.__emitter__ = new event.ReadyEmitter(); +for (_i = 0, _len = (_ref = ['on', 'addListener', 'off', 'removeListener', 'emit', 'trigger', 'once', 'removeAllListeners']).length; _i < _len; ++_i) { + k = _ref[_i]; + limn[k] = emitter[k].bind(emitter); +} +limn.mount = function(path){ + var mnt, _ref; + mnt = ((_ref = limn.config) != null ? _ref.mount : void 8) || '/'; + return (mnt !== '/' ? mnt : '') + path; +}; +Backbone = require('backbone'); +_ref = limn.base = require('./base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = limn.chart = require('./chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType; +_ref = limn.graph = require('./graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView, GraphEditView = _ref.GraphEditView, GraphListView = _ref.GraphListView; +_ref = limn.dashboard = require('./dashboard'), DashboardView = _ref.DashboardView, Dashboard = _ref.Dashboard; +/** + * @class Sets up root application, automatically attaching to an existing element + * found at `appSelector` and delegating to the appropriate view. + * @extends Backbone.Router + */ +LimnApp = limn.LimnApp = Backbone.Router.extend({ + appSelector: '#content .inner', + routes: { + 'graphs/(new|edit)': 'newGraph', + 'graphs/:graphId/edit': 'editGraph', + 'graphs/:graphId': 'showGraph', + 'graphs': 'listGraphs', + 'dashboards/(new|edit)': 'newDashboard', + 'dashboards/:dashId/edit': 'editDashboard', + 'dashboards/:dashId': 'showDashboard', + 'dashboards': 'listDashboards' + } + /** + * @constructor + */, + constructor: (function(){ + function LimnApp(config){ + var that; + this.config = config != null + ? config + : {}; + if (that = config.appSelector) { + this.appSelector = that; + } + this.el = config.el || (config.el = jQuery(this.appSelector)[0]); + this.$el = jQuery(this.el); + Backbone.Router.call(this, config); + return this; + } + return LimnApp; + }()), + initialize: function(){ + var _this = this; + jQuery(function(){ + return _this.setup(); + }); + return this; + }, + setup: function(){ + this.route(/^(?:[\?].*)?$/, 'home'); + return Backbone.history.start({ + pushState: true, + root: this.config.mount + }); + }, + processData: function(id, data){ + data == null && (data = {}); + if (!(id && _(['edit', 'new']).contains(id))) { + data.id = data.slug = id; + } + return data; + } + /* * * * Routes * * * */, + home: function(){ + return this.showDashboard('reportcard'); + }, + createGraphModel: function(id){ + var data, graph; + data = this.processData(id); + return graph = new Graph(data, { + parse: true + }); + }, + newGraph: function(){ + return this.editGraph(); + }, + editGraph: function(id){ + this.model = this.createGraphModel(id); + return this.view = new GraphEditView({ + model: this.model + }).attach(this.el); + }, + showGraph: function(id){ + this.model = this.createGraphModel(id); + return this.view = new GraphDisplayView({ + model: this.model + }).attach(this.el); + }, + listGraphs: function(){ + this.collection = new GraphList(); + return this.view = new GraphListView({ + collection: this.collection + }).attach(this.el); + }, + createDashboardModel: function(id){ + var data, dashboard; + data = this.processData(id); + return dashboard = new Dashboard(data, { + parse: true + }); + }, + newDashboard: function(){ + return console.error('newDashboard!?'); + }, + editDashboard: function(id){ + return console.error('editDashboard!?'); + }, + showDashboard: function(id){ + this.model = this.createDashboardModel(id); + return this.view = new DashboardView({ + model: this.model + }).attach(this.el); + }, + listDashboards: function(){ + return console.error('listDashboards!?'); + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); +__import(LimnApp, { + findConfig: function(){ + var config; + config = root.limn_config || {}; + config.mount || (config.mount = "/"); + return config; + }, + main: (function(){ + function limnMain(){ + var config; + config = limn.config || (limn.config = LimnApp.findConfig()); + if (!config.libOnly) { + limn.app || (limn.app = new LimnApp(config)); + } + return limn.emit('main', limn.app); + } + return limnMain; + }()) +}); +jQuery(LimnApp.main); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/client.mod.js b/lib/client.mod.js new file mode 100644 index 0000000..2c73b2f --- /dev/null +++ b/lib/client.mod.js @@ -0,0 +1,166 @@ +require.define('/node_modules/limn/client.js', function(require, module, exports, __dirname, __filename, undefined){ + +var EventEmitter, op, event, root, limn, emitter, k, Backbone, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _, _i, _len; +EventEmitter = require('events').EventEmitter; +_ref = require('./util'), _ = _ref._, op = _ref.op, event = _ref.event, root = _ref.root; +limn = exports; +emitter = limn.__emitter__ = new event.ReadyEmitter(); +for (_i = 0, _len = (_ref = ['on', 'addListener', 'off', 'removeListener', 'emit', 'trigger', 'once', 'removeAllListeners']).length; _i < _len; ++_i) { + k = _ref[_i]; + limn[k] = emitter[k].bind(emitter); +} +limn.mount = function(path){ + var mnt, _ref; + mnt = ((_ref = limn.config) != null ? _ref.mount : void 8) || '/'; + return (mnt !== '/' ? mnt : '') + path; +}; +Backbone = require('backbone'); +_ref = limn.base = require('./base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = limn.chart = require('./chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType; +_ref = limn.graph = require('./graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView, GraphEditView = _ref.GraphEditView, GraphListView = _ref.GraphListView; +_ref = limn.dashboard = require('./dashboard'), DashboardView = _ref.DashboardView, Dashboard = _ref.Dashboard; +/** + * @class Sets up root application, automatically attaching to an existing element + * found at `appSelector` and delegating to the appropriate view. + * @extends Backbone.Router + */ +LimnApp = limn.LimnApp = Backbone.Router.extend({ + appSelector: '#content .inner', + routes: { + 'graphs/(new|edit)': 'newGraph', + 'graphs/:graphId/edit': 'editGraph', + 'graphs/:graphId': 'showGraph', + 'graphs': 'listGraphs', + 'dashboards/(new|edit)': 'newDashboard', + 'dashboards/:dashId/edit': 'editDashboard', + 'dashboards/:dashId': 'showDashboard', + 'dashboards': 'listDashboards' + } + /** + * @constructor + */, + constructor: (function(){ + function LimnApp(config){ + var that; + this.config = config != null + ? config + : {}; + if (that = config.appSelector) { + this.appSelector = that; + } + this.el = config.el || (config.el = jQuery(this.appSelector)[0]); + this.$el = jQuery(this.el); + Backbone.Router.call(this, config); + return this; + } + return LimnApp; + }()), + initialize: function(){ + var _this = this; + jQuery(function(){ + return _this.setup(); + }); + return this; + }, + setup: function(){ + this.route(/^(?:[\?].*)?$/, 'home'); + return Backbone.history.start({ + pushState: true, + root: this.config.mount + }); + }, + processData: function(id, data){ + data == null && (data = {}); + if (!(id && _(['edit', 'new']).contains(id))) { + data.id = data.slug = id; + } + return data; + } + /* * * * Routes * * * */, + home: function(){ + return this.showDashboard('reportcard'); + }, + createGraphModel: function(id){ + var data, graph; + data = this.processData(id); + return graph = new Graph(data, { + parse: true + }); + }, + newGraph: function(){ + return this.editGraph(); + }, + editGraph: function(id){ + this.model = this.createGraphModel(id); + return this.view = new GraphEditView({ + model: this.model + }).attach(this.el); + }, + showGraph: function(id){ + this.model = this.createGraphModel(id); + return this.view = new GraphDisplayView({ + model: this.model + }).attach(this.el); + }, + listGraphs: function(){ + this.collection = new GraphList(); + return this.view = new GraphListView({ + collection: this.collection + }).attach(this.el); + }, + createDashboardModel: function(id){ + var data, dashboard; + data = this.processData(id); + return dashboard = new Dashboard(data, { + parse: true + }); + }, + newDashboard: function(){ + return console.error('newDashboard!?'); + }, + editDashboard: function(id){ + return console.error('editDashboard!?'); + }, + showDashboard: function(id){ + this.model = this.createDashboardModel(id); + return this.view = new DashboardView({ + model: this.model + }).attach(this.el); + }, + listDashboards: function(){ + return console.error('listDashboards!?'); + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); +__import(LimnApp, { + findConfig: function(){ + var config; + config = root.limn_config || {}; + config.mount || (config.mount = "/"); + return config; + }, + main: (function(){ + function limnMain(){ + var config; + config = limn.config || (limn.config = LimnApp.findConfig()); + if (!config.libOnly) { + limn.app || (limn.app = new LimnApp(config)); + } + return limn.emit('main', limn.app); + } + return limnMain; + }()) +}); +jQuery(LimnApp.main); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/data/datasource-model.js b/lib/data/datasource-model.js index 9af7647..5dc2b7e 100644 --- a/lib/data/datasource-model.js +++ b/lib/data/datasource-model.js @@ -1,4 +1,5 @@ -var op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _; +var limn, op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _; +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; _ref = require('../util/timeseries'), TimeSeriesData = _ref.TimeSeriesData, CSVData = _ref.CSVData; _ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache; @@ -208,9 +209,11 @@ sourceCache = new ModelCache(DataSource, { ready: false, cache: ALL_SOURCES }); -$.getJSON('/datasources/all', function(data){ - ALL_SOURCES.reset(_.map(data, op.I)); - return sourceCache.triggerReady(); +limn.on('main', function(){ + return $.getJSON(limn.mount('/datasources/all'), function(data){ + ALL_SOURCES.reset(_.map(data, op.I)); + return sourceCache.triggerReady(); + }); }); DataSource.getAllSources = function(){ return ALL_SOURCES; diff --git a/lib/data/datasource-model.mod.js b/lib/data/datasource-model.mod.js index 30a8182..132f27c 100644 --- a/lib/data/datasource-model.mod.js +++ b/lib/data/datasource-model.mod.js @@ -1,6 +1,7 @@ require.define('/node_modules/limn/data/datasource-model.js', function(require, module, exports, __dirname, __filename, undefined){ -var op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _; +var limn, op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _; +limn = require('../client'); _ref = require('../util'), _ = _ref._, op = _ref.op; _ref = require('../util/timeseries'), TimeSeriesData = _ref.TimeSeriesData, CSVData = _ref.CSVData; _ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache; @@ -210,9 +211,11 @@ sourceCache = new ModelCache(DataSource, { ready: false, cache: ALL_SOURCES }); -$.getJSON('/datasources/all', function(data){ - ALL_SOURCES.reset(_.map(data, op.I)); - return sourceCache.triggerReady(); +limn.on('main', function(){ + return $.getJSON(limn.mount('/datasources/all'), function(data){ + ALL_SOURCES.reset(_.map(data, op.I)); + return sourceCache.triggerReady(); + }); }); DataSource.getAllSources = function(){ return ALL_SOURCES; diff --git a/lib/graph/graph-model.js b/lib/graph/graph-model.js index 79428cd..78110e5 100644 --- a/lib/graph/graph-model.js +++ b/lib/graph/graph-model.js @@ -1,5 +1,6 @@ -var Seq, Cascade, BaseModel, BaseList, ModelCache, ChartType, DataSet, root, Graph, GraphList, _ref, _; +var Seq, limn, Cascade, BaseModel, BaseList, ModelCache, ChartType, DataSet, root, Graph, GraphList, _ref, _; Seq = require('seq'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, Cascade = _ref.Cascade; _ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache; ChartType = require('../chart').ChartType; @@ -68,7 +69,7 @@ Graph = exports.Graph = BaseModel.extend({ }; }, url: function(){ - return this.urlRoot + "/" + this.get('slug') + ".json"; + return limn.mount(this.urlRoot) + "/" + this.get('slug') + ".json"; }, constructor: (function(){ function Graph(attributes, opts){ @@ -405,7 +406,7 @@ Graph = exports.Graph = BaseModel.extend({ toURL: function(action){ var slug, path; slug = this.get('slug'); - path = _.compact([this.urlRoot, slug, action]).join('/'); + path = limn.mount(_.compact([this.urlRoot, slug, action])).join('/'); return path + "?" + this.toKV({ keepSlug: !!slug }); @@ -414,7 +415,7 @@ Graph = exports.Graph = BaseModel.extend({ * @returns {String} Path portion of slug URL, e.g. /graphs/:slug */, toLink: function(){ - return this.urlRoot + "/" + this.get('slug'); + return limn.mount(this.urlRoot) + "/" + this.get('slug'); } /** * @returns {String} Permalinked URI, e.g. http://reportcard.wmflabs.org/:slug diff --git a/lib/graph/graph-model.mod.js b/lib/graph/graph-model.mod.js index c511d60..74625d9 100644 --- a/lib/graph/graph-model.mod.js +++ b/lib/graph/graph-model.mod.js @@ -1,7 +1,8 @@ require.define('/node_modules/limn/graph/graph-model.js', function(require, module, exports, __dirname, __filename, undefined){ -var Seq, Cascade, BaseModel, BaseList, ModelCache, ChartType, DataSet, root, Graph, GraphList, _ref, _; +var Seq, limn, Cascade, BaseModel, BaseList, ModelCache, ChartType, DataSet, root, Graph, GraphList, _ref, _; Seq = require('seq'); +limn = require('../client'); _ref = require('../util'), _ = _ref._, Cascade = _ref.Cascade; _ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache; ChartType = require('../chart').ChartType; @@ -70,7 +71,7 @@ Graph = exports.Graph = BaseModel.extend({ }; }, url: function(){ - return this.urlRoot + "/" + this.get('slug') + ".json"; + return limn.mount(this.urlRoot) + "/" + this.get('slug') + ".json"; }, constructor: (function(){ function Graph(attributes, opts){ @@ -407,7 +408,7 @@ Graph = exports.Graph = BaseModel.extend({ toURL: function(action){ var slug, path; slug = this.get('slug'); - path = _.compact([this.urlRoot, slug, action]).join('/'); + path = limn.mount(_.compact([this.urlRoot, slug, action])).join('/'); return path + "?" + this.toKV({ keepSlug: !!slug }); @@ -416,7 +417,7 @@ Graph = exports.Graph = BaseModel.extend({ * @returns {String} Path portion of slug URL, e.g. /graphs/:slug */, toLink: function(){ - return this.urlRoot + "/" + this.get('slug'); + return limn.mount(this.urlRoot) + "/" + this.get('slug'); } /** * @returns {String} Permalinked URI, e.g. http://reportcard.wmflabs.org/:slug diff --git a/lib/limn.js b/lib/limn.js index 979a5f9..ceb7666 100644 --- a/lib/limn.js +++ b/lib/limn.js @@ -1,7 +1,18 @@ -var limn, Backbone, op, root, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _; +var EventEmitter, op, event, root, limn, emitter, k, Backbone, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _, _i, _len; +EventEmitter = require('events').EventEmitter; +_ref = require('./util'), _ = _ref._, op = _ref.op, event = _ref.event, root = _ref.root; limn = exports; +emitter = limn.__emitter__ = new event.ReadyEmitter(); +for (_i = 0, _len = (_ref = ['on', 'addListener', 'off', 'removeListener', 'emit', 'trigger', 'once', 'removeAllListeners']).length; _i < _len; ++_i) { + k = _ref[_i]; + limn[k] = emitter[k].bind(emitter); +} +limn.mount = function(path){ + var mnt, _ref; + mnt = ((_ref = limn.config) != null ? _ref.mount : void 8) || '/'; + return (mnt !== '/' ? mnt : '') + path; +}; Backbone = require('backbone'); -_ref = limn.util = require('./util'), _ = _ref._, op = _ref.op, root = _ref.root; _ref = limn.base = require('./base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; _ref = limn.chart = require('./chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType; _ref = limn.graph = require('./graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView, GraphEditView = _ref.GraphEditView, GraphListView = _ref.GraphListView; @@ -136,8 +147,9 @@ __import(LimnApp, { var config; config = limn.config || (limn.config = LimnApp.findConfig()); if (!config.libOnly) { - return limn.app || (limn.app = new LimnApp(config)); + limn.app || (limn.app = new LimnApp(config)); } + return limn.emit('main', limn.app); } return limnMain; }()) diff --git a/lib/limn.mod.js b/lib/limn.mod.js index 904f2eb..ce7fd7b 100644 --- a/lib/limn.mod.js +++ b/lib/limn.mod.js @@ -1,9 +1,20 @@ require.define('/node_modules/limn/limn.js', function(require, module, exports, __dirname, __filename, undefined){ -var limn, Backbone, op, root, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _; +var EventEmitter, op, event, root, limn, emitter, k, Backbone, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, DashboardView, Dashboard, LimnApp, _ref, _, _i, _len; +EventEmitter = require('events').EventEmitter; +_ref = require('./util'), _ = _ref._, op = _ref.op, event = _ref.event, root = _ref.root; limn = exports; +emitter = limn.__emitter__ = new event.ReadyEmitter(); +for (_i = 0, _len = (_ref = ['on', 'addListener', 'off', 'removeListener', 'emit', 'trigger', 'once', 'removeAllListeners']).length; _i < _len; ++_i) { + k = _ref[_i]; + limn[k] = emitter[k].bind(emitter); +} +limn.mount = function(path){ + var mnt, _ref; + mnt = ((_ref = limn.config) != null ? _ref.mount : void 8) || '/'; + return (mnt !== '/' ? mnt : '') + path; +}; Backbone = require('backbone'); -_ref = limn.util = require('./util'), _ = _ref._, op = _ref.op, root = _ref.root; _ref = limn.base = require('./base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; _ref = limn.chart = require('./chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType; _ref = limn.graph = require('./graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView, GraphEditView = _ref.GraphEditView, GraphListView = _ref.GraphListView; @@ -138,8 +149,9 @@ __import(LimnApp, { var config; config = limn.config || (limn.config = LimnApp.findConfig()); if (!config.libOnly) { - return limn.app || (limn.app = new LimnApp(config)); + limn.app || (limn.app = new LimnApp(config)); } + return limn.emit('main', limn.app); } return limnMain; }()) diff --git a/lib/server/middleware.js b/lib/server/middleware.js index 96273d8..de5d5fc 100644 --- a/lib/server/middleware.js +++ b/lib/server/middleware.js @@ -128,14 +128,21 @@ application = limn.application = { this.set('limn options', opts); mkdirp(opts.dataDir); this.configure(function(){ + var view_opts; this.set('views', WWW); this.set('view engine', 'jade'); - this.set('view options', __import({ + view_opts = __import({ layout: false, + config: this.set('limn options'), version: REV, IS_DEV: IS_DEV, - IS_PROD: IS_PROD - }, require('./view-helpers'))); + IS_PROD: IS_PROD, + REV: REV + }, require('./view-helpers')); + view_opts.__defineGetter__('mount', function(){ + return app.route || '/'; + }); + this.set('view options', view_opts); this.use(require('./reqinfo')({})); this.use(express.bodyParser()); this.use(express.methodOverride()); diff --git a/lib/server/server.js b/lib/server/server.js index 9b65e75..f4ac278 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -12,14 +12,15 @@ app = exports = module.exports = express.createServer(); /** * Load Limn middleware */ -app.use(limn = app.limn = LimnMiddleware({ +limn = app.limn = LimnMiddleware({ varDir: './var', dataDir: './var/data', proxy: { enabled: true, whitelist: /.*/ } -})); +}); +app.use(limn); app.use(express.errorHandler({ dumpExceptions: true, showStack: true diff --git a/lib/util/index.js b/lib/util/index.js index 8185164..36fc44b 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -1,4 +1,4 @@ -var op, root, backbone, parser, Cascade, _, _ref, __slice = [].slice; +var op, root, event, backbone, parser, Cascade, _, _ref, __slice = [].slice; _ = exports._ = require('./underscore'); op = exports.op = require('./op'); root = exports.root = function(){ @@ -32,7 +32,8 @@ if ((_ref = root.jQuery) != null) { return _results; }; } -__import(exports, require('./event')); +event = exports.event = require('./event'); +__import(exports, event); backbone = exports.backbone = require('./backbone'); parser = exports.parser = require('./parser'); Cascade = exports.Cascade = require('./cascade'); diff --git a/lib/util/index.mod.js b/lib/util/index.mod.js index 7c5fe4b..8013081 100644 --- a/lib/util/index.mod.js +++ b/lib/util/index.mod.js @@ -1,6 +1,6 @@ require.define('/node_modules/limn/util/index.js', function(require, module, exports, __dirname, __filename, undefined){ -var op, root, backbone, parser, Cascade, _, _ref, __slice = [].slice; +var op, root, event, backbone, parser, Cascade, _, _ref, __slice = [].slice; _ = exports._ = require('./underscore'); op = exports.op = require('./op'); root = exports.root = function(){ @@ -34,7 +34,8 @@ if ((_ref = root.jQuery) != null) { return _results; }; } -__import(exports, require('./event')); +event = exports.event = require('./event'); +__import(exports, event); backbone = exports.backbone = require('./backbone'); parser = exports.parser = require('./parser'); Cascade = exports.Cascade = require('./cascade'); diff --git a/lib/util/parser.js b/lib/util/parser.js index e2fa110..10c94bd 100644 --- a/lib/util/parser.js +++ b/lib/util/parser.js @@ -1,7 +1,6 @@ -var op, BaseModel, BaseList, BaseView, Mixin, Parsers, ParserMixin, ParsingModel, ParsingList, ParsingView, _, _ref; +var op, Parsers, _; _ = require('./underscore'); op = require('./op'); -_ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin; /** * @namespace Parsers by type. */ @@ -74,100 +73,4 @@ Parsers = exports.Parsers = { } } }; -Parsers.parseNumber = Parsers.parseFloat; -/** - * @class Methods for a class to select parsers by type reflection. - * @mixin - */ -exports.ParserMixin = ParserMixin = (function(superclass){ - ParserMixin.displayName = 'ParserMixin'; - var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin; - __import(ParserMixin.prototype, Parsers); - function ParserMixin(target){ - return Mixin.call(ParserMixin, target); - } - prototype.parseValue = function(v, type){ - return this.getParser(type)(v); - }; - prototype.getParser = function(type){ - var fn, t, _i, _ref, _len; - type == null && (type = 'String'); - fn = this["parse" + type]; - if (typeof fn === 'function') { - return fn; - } - type = _(String(type).toLowerCase()); - for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) { - t = _ref[_i]; - if (type.startsWith(t.toLowerCase())) { - return this["parse" + t]; - } - } - return this.defaultParser || this.parseString; - }; - prototype.getParserFromExample = function(v){ - var type; - if (v == null) { - return null; - } - type = typeof v; - if (type !== 'object') { - return this.getParser(type); - } else if (_.isArray(v)) { - return this.getParser('Array'); - } else { - return this.getParser('Object'); - } - }; - return ParserMixin; -}(Mixin)); -/** - * @class Basic model which mixes in the ParserMixin. - * @extends BaseModel - * @borrows ParserMixin - */ -ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingModel(){ - return BaseModel.apply(this, arguments); - } - return ParsingModel; - }()) -})); -/** - * @class Basic collection which mixes in the ParserMixin. - * @extends BaseList - * @borrows ParserMixin - */ -ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingList(){ - return BaseList.apply(this, arguments); - } - return ParsingList; - }()) -})); -/** - * @class Basic view which mixes in the ParserMixin. - * @extends BaseView - * @borrows ParserMixin - */ -ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingView(){ - return BaseView.apply(this, arguments); - } - return ParsingView; - }()) -})); -function __extend(sub, sup){ - function fun(){} fun.prototype = (sub.superclass = sup).prototype; - (sub.prototype = new fun).constructor = sub; - if (typeof sup.extended == 'function') sup.extended(sub); - return sub; -} -function __import(obj, src){ - var own = {}.hasOwnProperty; - for (var key in src) if (own.call(src, key)) obj[key] = src[key]; - return obj; -} \ No newline at end of file +Parsers.parseNumber = Parsers.parseFloat; \ No newline at end of file diff --git a/lib/util/parser.mod.js b/lib/util/parser.mod.js index a6dda3c..0c26c28 100644 --- a/lib/util/parser.mod.js +++ b/lib/util/parser.mod.js @@ -1,9 +1,8 @@ require.define('/node_modules/limn/util/parser.js', function(require, module, exports, __dirname, __filename, undefined){ -var op, BaseModel, BaseList, BaseView, Mixin, Parsers, ParserMixin, ParsingModel, ParsingList, ParsingView, _, _ref; +var op, Parsers, _; _ = require('./underscore'); op = require('./op'); -_ref = require('../base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin; /** * @namespace Parsers by type. */ @@ -77,101 +76,5 @@ Parsers = exports.Parsers = { } }; Parsers.parseNumber = Parsers.parseFloat; -/** - * @class Methods for a class to select parsers by type reflection. - * @mixin - */ -exports.ParserMixin = ParserMixin = (function(superclass){ - ParserMixin.displayName = 'ParserMixin'; - var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin; - __import(ParserMixin.prototype, Parsers); - function ParserMixin(target){ - return Mixin.call(ParserMixin, target); - } - prototype.parseValue = function(v, type){ - return this.getParser(type)(v); - }; - prototype.getParser = function(type){ - var fn, t, _i, _ref, _len; - type == null && (type = 'String'); - fn = this["parse" + type]; - if (typeof fn === 'function') { - return fn; - } - type = _(String(type).toLowerCase()); - for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) { - t = _ref[_i]; - if (type.startsWith(t.toLowerCase())) { - return this["parse" + t]; - } - } - return this.defaultParser || this.parseString; - }; - prototype.getParserFromExample = function(v){ - var type; - if (v == null) { - return null; - } - type = typeof v; - if (type !== 'object') { - return this.getParser(type); - } else if (_.isArray(v)) { - return this.getParser('Array'); - } else { - return this.getParser('Object'); - } - }; - return ParserMixin; -}(Mixin)); -/** - * @class Basic model which mixes in the ParserMixin. - * @extends BaseModel - * @borrows ParserMixin - */ -ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingModel(){ - return BaseModel.apply(this, arguments); - } - return ParsingModel; - }()) -})); -/** - * @class Basic collection which mixes in the ParserMixin. - * @extends BaseList - * @borrows ParserMixin - */ -ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingList(){ - return BaseList.apply(this, arguments); - } - return ParsingList; - }()) -})); -/** - * @class Basic view which mixes in the ParserMixin. - * @extends BaseView - * @borrows ParserMixin - */ -ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({ - constructor: (function(){ - function ParsingView(){ - return BaseView.apply(this, arguments); - } - return ParsingView; - }()) -})); -function __extend(sub, sup){ - function fun(){} fun.prototype = (sub.superclass = sup).prototype; - (sub.prototype = new fun).constructor = sub; - if (typeof sup.extended == 'function') sup.extended(sub); - return sub; -} -function __import(obj, src){ - var own = {}.hasOwnProperty; - for (var key in src) if (own.call(src, key)) obj[key] = src[key]; - return obj; -} }); diff --git a/src/base/base-model.co b/src/base/base-model.co index 7219b6a..a60acc9 100644 --- a/src/base/base-model.co +++ b/src/base/base-model.co @@ -1,5 +1,6 @@ Backbone = require 'backbone' +limn = require '../client' { _, op, } = require '../util' { BaseBackboneMixin, mixinBase, @@ -26,7 +27,7 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ ### Accessors url: -> - "#{@urlRoot}/#{@get('id')}.json" + "#{limn.mount @urlRoot}/#{@get('id')}.json" has: (key) -> @get(key)? @@ -228,11 +229,12 @@ BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{ @models.map -> it.id or it.get('id') or it.cid url: -> + root = limn.mount @urlRoot id = @get('id') or @get('slug') if id - "#{@urlRoot}/#id.json" + "#root/#id.json" else - "#{@urlRoot}.json" + "#root.json" load: -> diff --git a/src/base/base.co b/src/base/base.co index 5224bca..aa20b32 100644 --- a/src/base/base.co +++ b/src/base/base.co @@ -25,7 +25,7 @@ class Base extends EventEmitter @__superclass__ = @..superclass @__apply_bind__() super() - @__class__.emit 'new', this + @__class__.emit? 'new', this ### Auto-Bound methods @@ -44,6 +44,7 @@ class Base extends EventEmitter _.bindAll this, ...names if names.length + ### Misc getClassName: -> "#{@..name or @..displayName}" @@ -57,8 +58,8 @@ class Base extends EventEmitter @extended = (Subclass) -> # copy over all class methods, including this - for own k, v in this - Subclass[k] = v if typeof v is 'function' + for k, v in this + Subclass[k] = v if typeof v is 'function' and not _.contains <[ apply call constructor toString ]>, k Subclass.__super__ = @:: Subclass diff --git a/src/base/index.co b/src/base/index.co index 4a62daf..0d5abc8 100644 --- a/src/base/index.co +++ b/src/base/index.co @@ -7,3 +7,6 @@ cascading = require './cascading-model' data_binding = require './data-binding' exports import mixins import models import views \ import cache import cascading import data_binding + +parser_mixin = require './parser-mixin' +exports import parser_mixin diff --git a/src/base/parser-mixin.co b/src/base/parser-mixin.co new file mode 100644 index 0000000..d82d255 --- /dev/null +++ b/src/base/parser-mixin.co @@ -0,0 +1,86 @@ +{ Parsers, +} = require '../util/parser' +{ BaseModel, BaseList, BaseView, Mixin, +} = require '../base' + +exports.Parsers = Parsers + +/** + * @class Methods for a class to select parsers by type reflection. + * @mixin + */ +class exports.ParserMixin extends Mixin + this:: import Parsers + + (target) -> + return Mixin.call ParserMixin, target + + + # XXX: So I'm meh about mixing in the Parsers dictionary. + # + # - Pros: mixing in `parseXXX()` methods makes it easy to + # override in the target class. + # - Cons: `parse()` is a Backbone method, which bit me once + # already (hence `parseValue()`), so conflicts aren't unlikely. + # + # Other ideas: + # - Parsers live at `@__parsers__`, and each instance gets its own clone + # -> Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?) + + parseValue: (v, type) -> + @getParser(type)(v) + + getParser: (type='String') -> + # If this is a known type and we have a parser for it, return that + fn = @["parse#type"] + return fn if typeof fn is 'function' + + # Handle compound/optional types + # XXX: handle 'or' by returning an array of parsers? + type = _ String(type).toLowerCase() + for t of <[ Integer Float Number Boolean Object Array Function ]> + if type.startsWith t.toLowerCase() + return @["parse#t"] + @defaultParser or @parseString + + getParserFromExample: (v) -> + return null unless v? + type = typeof v + + if type is not 'object' + @getParser type + else if _.isArray v + @getParser 'Array' + else + @getParser 'Object' + + + + +/** + * @class Basic model which mixes in the ParserMixin. + * @extends BaseModel + * @borrows ParserMixin + */ +ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin.mix do + constructor: function ParsingModel then BaseModel ... + + +/** + * @class Basic collection which mixes in the ParserMixin. + * @extends BaseList + * @borrows ParserMixin + */ +ParsingList = exports.ParsingList = BaseList.extend ParserMixin.mix do + constructor: function ParsingList then BaseList ... + + +/** + * @class Basic view which mixes in the ParserMixin. + * @extends BaseView + * @borrows ParserMixin + */ +ParsingView = exports.ParsingView = BaseView.extend ParserMixin.mix do + constructor: function ParsingView then BaseView ... + + diff --git a/src/chart/chart-type.co b/src/chart/chart-type.co index adbfc92..7d45cf8 100644 --- a/src/chart/chart-type.co +++ b/src/chart/chart-type.co @@ -1,12 +1,13 @@ moment = require 'moment' Backbone = require 'backbone' +limn = require '../client' { _, op, } = require '../util' { ReadyEmitter, } = require '../util/event' { Parsers, ParserMixin, -} = require '../util/parser' +} = require '../base' @@ -159,11 +160,12 @@ class exports.ChartType extends ReadyEmitter */ loadSpec: -> return this if @ready - proto = @constructor:: + proto = @constructor:: + url = (if limn.config.mount is not '/' then limn.config.mount else '') + @SPEC_URL jQuery.ajax do - url : @SPEC_URL + url : url dataType : 'json' - success : (spec) ~> + success : (spec) ~> proto.spec = spec proto.options_ordered = spec proto.options = _.synthesize spec, -> [it.name, it] diff --git a/src/chart/option/chart-option-model.co b/src/chart/option/chart-option-model.co index 458133b..5f7a5db 100644 --- a/src/chart/option/chart-option-model.co +++ b/src/chart/option/chart-option-model.co @@ -1,8 +1,8 @@ { _, op, } = require '../../util' -{ Parsers, ParserMixin, ParsingModel, ParsingView, -} = require '../../util/parser' -{ BaseModel, BaseList, +{ + BaseModel, BaseList, + Parsers, ParserMixin, ParsingModel, ParsingView, } = require '../../base' diff --git a/src/chart/type/d3-chart.co b/src/chart/type/d3-chart.co index 9f0e321..e4756cd 100644 --- a/src/chart/type/d3-chart.co +++ b/src/chart/type/d3-chart.co @@ -2,11 +2,11 @@ d3 = require 'd3' ColorBrewer = require 'colorbrewer' { _, op, -} = require '../../../util' +} = require '../../util' { ChartType, -} = require '../../chart-type' +} = require '../chart-type' { D3ChartElement, -} = require './d3-chart-element' +} = require './d3/d3-chart-element' root = do -> this diff --git a/src/chart/type/dygraphs.co b/src/chart/type/dygraphs.co index 00a0bcd..8490e1b 100644 --- a/src/chart/type/dygraphs.co +++ b/src/chart/type/dygraphs.co @@ -1,6 +1,6 @@ -_ = require '../../../util/underscore' +_ = require '../../util/underscore' { ChartType, -} = require '../../chart-type' +} = require '../chart-type' class exports.DygraphsChartType extends ChartType diff --git a/src/client.co b/src/client.co new file mode 100644 index 0000000..22acef6 --- /dev/null +++ b/src/client.co @@ -0,0 +1,161 @@ +{EventEmitter} = require 'events' + +{ _, op, event, root, +} = require './util' + +# Decorate root limn namespace object with EventEmitter methods +limn = exports +emitter = limn.__emitter__ = new event.ReadyEmitter() +for k of <[ on addListener off removeListener emit trigger once removeAllListeners ]> + limn[k] = emitter[k].bind emitter + +limn.mount = (path) -> + mnt = limn.config?.mount or '/' + (if mnt is not '/' then mnt else '') + path + + +Backbone = require 'backbone' + +{ BaseView, BaseModel, BaseList, +} = limn.base = require './base' +{ ChartType, DygraphsChartType, +} = limn.chart = require './chart' +{ Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, +} = limn.graph = require './graph' +{ DashboardView, Dashboard, +} = limn.dashboard = require './dashboard' + + +/** + * @class Sets up root application, automatically attaching to an existing element + * found at `appSelector` and delegating to the appropriate view. + * @extends Backbone.Router + */ +LimnApp = limn.LimnApp = Backbone.Router.extend do # {{{ + appSelector : '#content .inner' + + routes: + 'graphs/(new|edit)' : 'newGraph' + 'graphs/:graphId/edit' : 'editGraph' + 'graphs/:graphId' : 'showGraph' + 'graphs' : 'listGraphs' + + 'dashboards/(new|edit)' : 'newDashboard' + 'dashboards/:dashId/edit' : 'editDashboard' + 'dashboards/:dashId' : 'showDashboard' + 'dashboards' : 'listDashboards' + + + + /** + * @constructor + */ + constructor: function LimnApp (@config={}) + @appSelector = that if config.appSelector + @el = config.el or= jQuery @appSelector .0 + @$el = jQuery @el + Backbone.Router.call this, config + this + + initialize: -> + jQuery ~> @setup() + this + + setup: -> + # Add / route for Homepage + @route /^(?:[\?].*)?$/, 'home' + # Start observing history changes + Backbone.history.start { +pushState, root:@config.mount } + + # Helper for setting up models + processData: (id, data={}) -> + ### If we got querystring args, apply them as overrides to the data + # loc = String root.location + # if loc.split '?' .1 + # data = _.uncollapseObject _.fromKV that.replace('#', '%23') + # data.parents = JSON.parse that if data.parents + # data.options = _.synthesize do + # data.options or {} + # (v, k) -> [ k, dyglib.parseOption(k,v) ] + unless id and _ <[ edit new ]> .contains id + data.id = data.slug = id + data + + + + /* * * * Routes * * * */ + + + home: -> + # console.log "#this.home!" + @showDashboard 'reportcard' + + + ### Graphs + + createGraphModel: (id) -> + data = @processData id + graph = new Graph data, {+parse} + + newGraph: -> + @editGraph() + + editGraph: (id) -> + @model = @createGraphModel id + @view = new GraphEditView {@model} .attach @el + + showGraph: (id) -> + @model = @createGraphModel id + @view = new GraphDisplayView {@model} .attach @el + + listGraphs: -> + @collection = new GraphList() + @view = new GraphListView {@collection} .attach @el + + + ### Dashboards + + createDashboardModel: (id) -> + data = @processData id + dashboard = new Dashboard data, {+parse} + + newDashboard: -> + console.error 'newDashboard!?' + + editDashboard: (id) -> + console.error 'editDashboard!?' + + showDashboard: (id) -> + @model = @createDashboardModel id + @view = new DashboardView {@model} .attach @el + + listDashboards: -> + console.error 'listDashboards!?' + + + ### Misc + + getClassName: -> + "#{@..name or @..displayName}" + + toString: -> + "#{@getClassName()}()" +# }}} + + +### Static Methods +LimnApp import do + + findConfig : -> + # TODO: fill out inferred config + config = root.limn_config or {} + config.mount or= "/" + config + + main : function limnMain + config = limn.config or= LimnApp.findConfig() + limn.app or= new LimnApp config unless config.libOnly + limn.emit 'main', limn.app + + +jQuery LimnApp.main diff --git a/src/data/datasource-model.co b/src/data/datasource-model.co index 84c3b52..d3378ac 100644 --- a/src/data/datasource-model.co +++ b/src/data/datasource-model.co @@ -1,3 +1,4 @@ +limn = require '../client' { _, op, } = require '../util' { TimeSeriesData, CSVData, @@ -180,9 +181,10 @@ ALL_SOURCES = new DataSourceList sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES} # Fetch all DataSources -$.getJSON '/datasources/all', (data) -> - ALL_SOURCES.reset _.map data, op.I - sourceCache.triggerReady() +limn.on 'main', -> + $.getJSON limn.mount('/datasources/all'), (data) -> + ALL_SOURCES.reset _.map data, op.I + sourceCache.triggerReady() DataSource.getAllSources = -> ALL_SOURCES diff --git a/src/graph/graph-model.co b/src/graph/graph-model.co index 5d825d5..911e1bf 100644 --- a/src/graph/graph-model.co +++ b/src/graph/graph-model.co @@ -1,5 +1,6 @@ Seq = require 'seq' +limn = require '../client' { _, Cascade, } = require '../util' { BaseModel, BaseList, ModelCache, @@ -79,7 +80,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ options : {} url: -> - "#{@urlRoot}/#{@get('slug')}.json" + "#{limn.mount @urlRoot}/#{@get('slug')}.json" @@ -396,14 +397,14 @@ Graph = exports.Graph = BaseModel.extend do # {{{ */ toURL: (action) -> slug = @get 'slug' - path = _.compact [ @urlRoot, slug, action ] .join '/' + path = limn.mount _.compact [ @urlRoot, slug, action ] .join '/' "#path?#{@toKV { keepSlug: !!slug }}" /** * @returns {String} Path portion of slug URL, e.g. /graphs/:slug */ toLink: -> - "#{@urlRoot}/#{@get('slug')}" + "#{limn.mount @urlRoot}/#{@get('slug')}" /** * @returns {String} Permalinked URI, e.g. http://reportcard.wmflabs.org/:slug diff --git a/src/limn.co b/src/limn.co deleted file mode 100644 index a7224e1..0000000 --- a/src/limn.co +++ /dev/null @@ -1,149 +0,0 @@ -limn = exports - -Backbone = require 'backbone' - -{ _, op, root, -} = limn.util = require './util' -{ BaseView, BaseModel, BaseList, -} = limn.base = require './base' -{ ChartType, DygraphsChartType, -} = limn.chart = require './chart' -{ Graph, GraphList, GraphDisplayView, GraphEditView, GraphListView, -} = limn.graph = require './graph' -{ DashboardView, Dashboard, -} = limn.dashboard = require './dashboard' - - -/** - * @class Sets up root application, automatically attaching to an existing element - * found at `appSelector` and delegating to the appropriate view. - * @extends Backbone.Router - */ -LimnApp = limn.LimnApp = Backbone.Router.extend do # {{{ - appSelector : '#content .inner' - - routes: - 'graphs/(new|edit)' : 'newGraph' - 'graphs/:graphId/edit' : 'editGraph' - 'graphs/:graphId' : 'showGraph' - 'graphs' : 'listGraphs' - - 'dashboards/(new|edit)' : 'newDashboard' - 'dashboards/:dashId/edit' : 'editDashboard' - 'dashboards/:dashId' : 'showDashboard' - 'dashboards' : 'listDashboards' - - - - /** - * @constructor - */ - constructor: function LimnApp (@config={}) - @appSelector = that if config.appSelector - @el = config.el or= jQuery @appSelector .0 - @$el = jQuery @el - Backbone.Router.call this, config - this - - initialize: -> - jQuery ~> @setup() - this - - setup: -> - # Add / route for Homepage - @route /^(?:[\?].*)?$/, 'home' - # Start observing history changes - Backbone.history.start { +pushState, root:@config.mount } - - # Helper for setting up models - processData: (id, data={}) -> - ### If we got querystring args, apply them as overrides to the data - # loc = String root.location - # if loc.split '?' .1 - # data = _.uncollapseObject _.fromKV that.replace('#', '%23') - # data.parents = JSON.parse that if data.parents - # data.options = _.synthesize do - # data.options or {} - # (v, k) -> [ k, dyglib.parseOption(k,v) ] - unless id and _ <[ edit new ]> .contains id - data.id = data.slug = id - data - - - - /* * * * Routes * * * */ - - - home: -> - # console.log "#this.home!" - @showDashboard 'reportcard' - - - ### Graphs - - createGraphModel: (id) -> - data = @processData id - graph = new Graph data, {+parse} - - newGraph: -> - @editGraph() - - editGraph: (id) -> - @model = @createGraphModel id - @view = new GraphEditView {@model} .attach @el - - showGraph: (id) -> - @model = @createGraphModel id - @view = new GraphDisplayView {@model} .attach @el - - listGraphs: -> - @collection = new GraphList() - @view = new GraphListView {@collection} .attach @el - - - ### Dashboards - - createDashboardModel: (id) -> - data = @processData id - dashboard = new Dashboard data, {+parse} - - newDashboard: -> - console.error 'newDashboard!?' - - editDashboard: (id) -> - console.error 'editDashboard!?' - - showDashboard: (id) -> - @model = @createDashboardModel id - @view = new DashboardView {@model} .attach @el - - listDashboards: -> - console.error 'listDashboards!?' - - - ### Misc - - getClassName: -> - "#{@..name or @..displayName}" - - toString: -> - "#{@getClassName()}()" -# }}} - - -### Static Methods -LimnApp import do - - findConfig : -> - # TODO: fill out inferred config - config = root.limn_config or {} - config.mount or= "/" - config - - main : function limnMain - config = limn.config or= LimnApp.findConfig() - limn.app or= new LimnApp config unless config.libOnly - - -jQuery LimnApp.main - diff --git a/src/server/index.js b/src/server/index.js index 37f5743..c9e5b08 100755 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,6 +1,5 @@ #!/usr/bin/env node var coco = require('coco') -, coffee = require('coffee-script') , server = require('./server') ; diff --git a/src/server/middleware.co b/src/server/middleware.co index aa15ec1..69c1eb6 100755 --- a/src/server/middleware.co +++ b/src/server/middleware.co @@ -153,12 +153,15 @@ application = limn.application = @configure -> @set 'views', WWW @set 'view engine', 'jade' - @set 'view options', { + + view_opts = { layout : false + config : @set('limn options') version : REV - IS_DEV, IS_PROD + IS_DEV, IS_PROD, REV } import require './view-helpers' - # @helpers require './view-helpers' + view_opts.__defineGetter__ 'mount', -> app.route or '/' + @set 'view options', view_opts # Parse URL, fiddle @use require('./reqinfo')({}) diff --git a/src/server/server.co b/src/server/server.co index 2aa38db..ca82510 100755 --- a/src/server/server.co +++ b/src/server/server.co @@ -17,13 +17,16 @@ app = exports = module.exports = express.createServer() /** * Load Limn middleware */ -app.use limn = app.limn = LimnMiddleware do +limn = app.limn = LimnMiddleware do varDir : './var' dataDir : './var/data' proxy : enabled : true whitelist : /.*/ +app.use limn +# app.use '/vis', limn + # show exceptions, pretty stack traces ### FIXME app.use express.errorHandler { +dumpExceptions, +showStack } diff --git a/src/util/index.co b/src/util/index.co index 1686b86..48f813f 100644 --- a/src/util/index.co +++ b/src/util/index.co @@ -26,7 +26,8 @@ root.jQuery?.fn.invoke = (method, ...args) -> jQuery(el)[method] ...args -exports import require './event' +event = exports.event = require './event' +exports import event backbone = exports.backbone = require './backbone' parser = exports.parser = require './parser' diff --git a/src/util/parser.co b/src/util/parser.co index b9879f3..e951343 100644 --- a/src/util/parser.co +++ b/src/util/parser.co @@ -1,7 +1,5 @@ _ = require './underscore' op = require './op' -{ BaseModel, BaseList, BaseView, Mixin, -} = require '../base' /** @@ -45,83 +43,3 @@ Parsers = exports.Parsers = # Aliases Parsers.parseNumber = Parsers.parseFloat - -/** - * @class Methods for a class to select parsers by type reflection. - * @mixin - */ -class exports.ParserMixin extends Mixin - this:: import Parsers - - (target) -> - return Mixin.call ParserMixin, target - - - # XXX: So I'm meh about mixing in the Parsers dictionary. - # - # - Pros: mixing in `parseXXX()` methods makes it easy to - # override in the target class. - # - Cons: `parse()` is a Backbone method, which bit me once - # already (hence `parseValue()`), so conflicts aren't unlikely. - # - # Other ideas: - # - Parsers live at `@__parsers__`, and each instance gets its own clone - # -> Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?) - - parseValue: (v, type) -> - @getParser(type)(v) - - getParser: (type='String') -> - # If this is a known type and we have a parser for it, return that - fn = @["parse#type"] - return fn if typeof fn is 'function' - - # Handle compound/optional types - # XXX: handle 'or' by returning an array of parsers? - type = _ String(type).toLowerCase() - for t of <[ Integer Float Number Boolean Object Array Function ]> - if type.startsWith t.toLowerCase() - return @["parse#t"] - @defaultParser or @parseString - - getParserFromExample: (v) -> - return null unless v? - type = typeof v - - if type is not 'object' - @getParser type - else if _.isArray v - @getParser 'Array' - else - @getParser 'Object' - - - - -/** - * @class Basic model which mixes in the ParserMixin. - * @extends BaseModel - * @borrows ParserMixin - */ -ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin.mix do - constructor: function ParsingModel then BaseModel ... - - -/** - * @class Basic collection which mixes in the ParserMixin. - * @extends BaseList - * @borrows ParserMixin - */ -ParsingList = exports.ParsingList = BaseList.extend ParserMixin.mix do - constructor: function ParsingList then BaseList ... - - -/** - * @class Basic view which mixes in the ParserMixin. - * @extends BaseView - * @borrows ParserMixin - */ -ParsingView = exports.ParsingView = BaseView.extend ParserMixin.mix do - constructor: function ParsingView then BaseView ... - - diff --git a/src/version.js b/src/version.js index 920505f..f51e45a 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -module.exports = exports = '9c55933'; +module.exports = exports = '6e6fd02'; diff --git a/www/css/hicons.css b/www/css/hicons.css index 79f89e5..323c837 100644 --- a/www/css/hicons.css +++ b/www/css/hicons.css @@ -3,7 +3,7 @@ display: inline-block; vertical-align: text-top; background-repeat: no-repeat; - background-image: url("/img/hicons/hicons-sprite-black.png"); + background-image: url("../img/hicons/hicons-sprite-black.png"); width: 32px; height: 32px; line-height: 32px; @@ -12,7 +12,7 @@ } [class^="hicon-"][class~="icon-white"], [class*=" hicon-"][class~="icon-white"] { - background-image: url("/img/hicons/hicons-sprite-white.png"); + background-image: url("../img/hicons/hicons-sprite-white.png"); } [class^="hicon-"][class~="icon-sm"], [class*=" hicon-"][class~="icon-sm"] { diff --git a/www/css/hicons.styl b/www/css/hicons.styl index bcdb734..cb6da3c 100644 --- a/www/css/hicons.styl +++ b/www/css/hicons.styl @@ -5,10 +5,10 @@ display inline-block vertical-align text-top background-repeat no-repeat - background-image url("/img/hicons/hicons-sprite-black.png") + background-image url("../img/hicons/hicons-sprite-black.png") &[class~="icon-white"] - background-image url("/img/hicons/hicons-sprite-white.png") + background-image url("../img/hicons/hicons-sprite-white.png") // Large-size (32px) icons have no suffix width 32px diff --git a/www/css/layout.css b/www/css/layout.css index 62aa8ba..7831353 100644 --- a/www/css/layout.css +++ b/www/css/layout.css @@ -159,7 +159,7 @@ footer h4 a:active { height: 17px; line-height: 16px; background: transparent no-repeat 0 0; - background-image: url("/img/wmf_logo/wmf_logo-white-16x16.png") !important; + background-image: url("../img/wmf_logo/wmf_logo-white-16x16.png") !important; } .image { display: block; @@ -172,7 +172,7 @@ footer h4 a:active { width: 45px; height: 45px; line-height: 45px; - background-image: url("/img/wmf_logo/wmf_logo-black-45x45-a100.png"); + background-image: url("../img/wmf_logo/wmf_logo-black-45x45-a100.png"); opacity: 0.1; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=10); } @@ -184,7 +184,7 @@ footer h4 a:active { width: 31px; height: 32px; line-height: 32px; - background-image: url("/img/public_domain/public_domain-31x32-a100.png"); + background-image: url("../img/public_domain/public_domain-31x32-a100.png"); opacity: 0.12; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=12); } diff --git a/www/css/layout.styl b/www/css/layout.styl index 742deb5..9b92ee3 100644 --- a/www/css/layout.styl +++ b/www/css/layout.styl @@ -12,7 +12,7 @@ height 17px line-height 16px background transparent no-repeat 0 0 - background-image url("/img/wmf_logo/wmf_logo-white-16x16.png") !important + background-image url("../img/wmf_logo/wmf_logo-white-16x16.png") !important .image @@ -30,7 +30,7 @@ width 45px height 45px line-height 45px - background-image url("/img/wmf_logo/wmf_logo-black-45x45-a100.png") + background-image url("../img/wmf_logo/wmf_logo-black-45x45-a100.png") opacity 0.10 &:hover opacity 0.25 @@ -39,7 +39,7 @@ width 31px height 32px line-height 32px - background-image url("/img/public_domain/public_domain-31x32-a100.png") + background-image url("../img/public_domain/public_domain-31x32-a100.png") opacity 0.12 &:hover opacity 0.25 diff --git a/www/footer.jade b/www/footer.jade new file mode 100644 index 0000000..d34150e --- /dev/null +++ b/www/footer.jade @@ -0,0 +1,87 @@ +.info-row.row-fluid + .site-map.col.span4 + h3 + i.hicon-chart-curve.icon-white.icon-sm + a(href="/") Limn + ul.site-level + li + i.icon-home.icon-white + a(href="/") Home + //- FIXME: put link to /graphs only if dev env, for now. + if IS_DEV + li + i.icon-th.icon-white + a(href='/graphs') Browse + li + i.icon-glass.icon-white + a(href="/about") About + ul + li: a(href="/contact") Contact + li: a(href="http://mediawiki.org/wiki/Analytics", target="_blank") Other Projects + li + i.icon-bookmark.icon-white + a(href="/project") Project + //- + ul + li: a(href="mailto:dsc@wikimedia.org?subject=Reportcard/Limn Feedback") Feedback! + li: a(href="/blog") Blog + li: a(href="/docs/roadmap") Roadmap + li: a(href="https://github.com/wikimedia/limn", target="_blank") Source Repository + li: a(href="https://github.com/wikimedia/limn/issues", target="_blank") Issue Tracker + li: a(href="https://lists.wikimedia.org/mailman/listinfo/analytics", target="_blank") Mailing List + //- + li + i.icon-question-sign.icon-white + a(href="/help") Help + + .get-involved.col.span4 + h3 + i.icon-comment.icon-white + a(href="/project") Get Involved! + p + a(href="/project") Limn + | is + a(href="https://github.com/wikimedia/limn", target="_blank") open-source software + | , made with love by the + a(href="/about") Wikimedia Analytics Team + | . + p + | Find a bug or have a suggestion? + a(href="mailto:dsc@wikimedia.org?subject=Limn Feedback") We'd love to hear from you! + + .about-wmf.col.span4 + h3 + i.icon-wmf.icon-white + a(href="http://wikimedia.org", target="_blank") The Wikimedia Movement + p + a(href="http://wikimediafoundation.org/wiki/Wikimedia:About", target="_blank") The Wikimedia Foundation + | is the non-profit organization that operates + a(href="http://wikipedia.org", target="_blank") Wikipedia + | and + a(href="http://wikimedia.org", target="_blank") other free knowledge projects + | . + p + | If you're excited about community analytics, check out some of the + a(href="http://stats.wikimedia.org", target="_blank") other stuff we're working on + | . + +.copyright-row.row-fluid + .copyright.inner(xmlns:dct="http://purl.org/dc/terms/") + a.public-domain.image(href="http://creativecommons.org/publicdomain/zero/1.0/", rel="license", title="Public Domain", target="_blank") + | To the extent possible under law, + a(href="http://www.wikimediafoundation.org", rel="dct:publisher", title="The Wikimedia Foundation", target="_blank") + span(property="dct:title") The Wikimedia Foundation + | has waived all copyright and related or neighboring rights to the + span(property="dct:title") Monthly Report Card data and charts + | . + +.tos-row.row-fluid + a(href="http://wikimediafoundation.org/wiki/Terms_of_use", target="_blank") Terms of Use + span.separator • + a(href="http://wikimediafoundation.org/wiki/Wikimedia:General_disclaimer", target="_blank") Disclaimers + span.separator • + a(href="http://wikimediafoundation.org/wiki/Wikimedia:Privacy_policy", target="_blank") Privacy Policy + +a.wmf-logo.image(href="http://mediawiki.org/wiki/Analytics", target="_blank", + title="A product of Team Analytics at the Wikimedia Foundation") + | A product of Team Analytics at the Wikimedia Foundation diff --git a/www/geo.jade b/www/geo.jade index ddcbd2b..d89645c 100644 --- a/www/geo.jade +++ b/www/geo.jade @@ -7,7 +7,7 @@ append styles mixin css('geo-display.css') block main-scripts - script(src="/js/limn/main-geo.js?"+version) + script(src=mount+"/js/limn/main-geo.js?"+version) block content section.geo diff --git a/www/layout.jade b/www/layout.jade index a307357..a7e58a3 100644 --- a/www/layout.jade +++ b/www/layout.jade @@ -31,108 +31,24 @@ html(lang="en", dir="ltr") footer block footer - .info-row.row-fluid - .site-map.col.span4 - h3 - i.hicon-chart-curve.icon-white.icon-sm - a(href="/") Limn - ul.site-level - li - i.icon-home.icon-white - a(href="/") Home - //- FIXME: put link to /graphs only if dev env, for now. - if IS_DEV - li - i.icon-th.icon-white - a(href='/graphs') Browse - li - i.icon-glass.icon-white - a(href="/about") About - ul - li: a(href="/contact") Contact - li: a(href="http://mediawiki.org/wiki/Analytics", target="_blank") Other Projects - li - i.icon-bookmark.icon-white - a(href="/project") Project - //- - ul - li: a(href="mailto:dsc@wikimedia.org?subject=Reportcard/Limn Feedback") Feedback! - li: a(href="/blog") Blog - li: a(href="/docs/roadmap") Roadmap - li: a(href="https://github.com/wikimedia/limn", target="_blank") Source Repository - li: a(href="https://github.com/wikimedia/limn/issues", target="_blank") Issue Tracker - li: a(href="https://lists.wikimedia.org/mailman/listinfo/analytics", target="_blank") Mailing List - //- - li - i.icon-question-sign.icon-white - a(href="/help") Help - - .get-involved.col.span4 - h3 - i.icon-comment.icon-white - a(href="/project") Get Involved! - p - a(href="/project") Limn - | is - a(href="https://github.com/wikimedia/limn", target="_blank") open-source software - | , made with love by the - a(href="/about") Wikimedia Analytics Team - | . - p - | Find a bug or have a suggestion? - a(href="mailto:dsc@wikimedia.org?subject=Limn Feedback") We'd love to hear from you! - - .about-wmf.col.span4 - h3 - i.icon-wmf.icon-white - a(href="http://wikimedia.org", target="_blank") The Wikimedia Movement - p - a(href="http://wikimediafoundation.org/wiki/Wikimedia:About", target="_blank") The Wikimedia Foundation - | is the non-profit organization that operates - a(href="http://wikipedia.org", target="_blank") Wikipedia - | and - a(href="http://wikimedia.org", target="_blank") other free knowledge projects - | . - p - | If you're excited about community analytics, check out some of the - a(href="http://stats.wikimedia.org", target="_blank") other stuff we're working on - | . - - .copyright-row.row-fluid - .copyright.inner(xmlns:dct="http://purl.org/dc/terms/") - a.public-domain.image(href="http://creativecommons.org/publicdomain/zero/1.0/", rel="license", title="Public Domain", target="_blank") - | To the extent possible under law, - a(href="http://www.wikimediafoundation.org", rel="dct:publisher", title="The Wikimedia Foundation", target="_blank") - span(property="dct:title") The Wikimedia Foundation - | has waived all copyright and related or neighboring rights to the - span(property="dct:title") Monthly Report Card data and charts - | . - - .tos-row.row-fluid - a(href="http://wikimediafoundation.org/wiki/Terms_of_use", target="_blank") Terms of Use - span.separator • - a(href="http://wikimediafoundation.org/wiki/Wikimedia:General_disclaimer", target="_blank") Disclaimers - span.separator • - a(href="http://wikimediafoundation.org/wiki/Wikimedia:Privacy_policy", target="_blank") Privacy Policy - - a.wmf-logo.image(href="http://mediawiki.org/wiki/Analytics", target="_blank", - title="A product of Team Analytics at the Wikimedia Foundation") - | A product of Team Analytics at the Wikimedia Foundation + include footer .scripts block scripts + - var limn_config = { 'mount':mount, 'rev':version, 'env':NODE_ENV }; script var VERSION = !{ JSON.stringify(version) }; var ENV = !{ JSON.stringify(NODE_ENV) }; var IS_DEV = ENV === 'development', IS_PROD = ENV === 'production'; + var limn_config = !{ JSON.stringify(limn_config) }; block lib-scripts for src in sources(WWW+'/modules.yaml') - script(src=src+"?"+version) + script(src=path.join(mount, src)+"?"+version) block page-scripts script - var limn = require('limn/limn'); + var limn = require('limn/client'); block main-scripts block addenda diff --git a/www/mixins/helpers.jade b/www/mixins/helpers.jade index a9ccc24..1850f11 100644 --- a/www/mixins/helpers.jade +++ b/www/mixins/helpers.jade @@ -1,12 +1,11 @@ -- root = (function(){ return this; })(); -- if (typeof version == 'undefined') root.version = 'HEAD'; -- if (version == null || !version || version === 'HEAD') { version = String(new Date().getTime()); } +- if (!locals.version) locals.version = 'HEAD'; +- if (version == null || !version || version === 'HEAD') { locals.version = String(new Date().getTime()); } mixin css(href, media, path_root) - media = media || 'screen' - - path_root = path_root || '/css' - - var ver = ((typeof version != 'undefined' && version) ? '?'+version : '') - - href = ((path_root && href.charAt(0) !== '/') ? path_root+'/' : '') + href + ver + - path_root = path_root || (href.charAt(0) === '/' ? locals.mount : locals.path.join(locals.mount, 'css')) + - var ver = (locals.version ? '?'+locals.version : '') + - href = locals.path.join(path_root, href) + ver link(type='text/css', rel='stylesheet', media=media, href=href) diff --git a/www/modules.yaml b/www/modules.yaml index 8909c72..3abe9b2 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -99,6 +99,7 @@ development: - data-binding - model-cache - cascading-model + - parser-mixin - index - graph: - graph-model @@ -139,7 +140,7 @@ development: - dashboard-model - dashboard-view - index - - limn + - client # - suffix: .js # paths: -- 1.7.0.4