booster 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +106 -0
- data/MIT-LICENSE +20 -0
- data/README.md +174 -0
- data/Rakefile +7 -0
- data/booster.gemspec +24 -0
- data/booster.tmbundle/Snippets/untitled.tmSnippet +14 -0
- data/booster.tmbundle/Syntaxes/Booster.tmLanguage +46 -0
- data/booster.tmbundle/info.plist +10 -0
- data/lib/assets/javascripts/booster-core.js +5 -0
- data/lib/assets/javascripts/booster-support.js +1 -0
- data/lib/assets/javascripts/booster.js +2 -0
- data/lib/assets/javascripts/booster/collection.js.boost +1 -0
- data/lib/assets/javascripts/booster/model.js.boost +30 -0
- data/lib/assets/javascripts/booster/router.js.boost +107 -0
- data/lib/assets/javascripts/booster/support/binding.js.boost +1 -0
- data/lib/assets/javascripts/booster/support/helpers.js.boost +109 -0
- data/lib/assets/javascripts/booster/support/i18n.js.boost +136 -0
- data/lib/assets/javascripts/booster/support/observer.js.boost +81 -0
- data/lib/assets/javascripts/booster/support/schema.js.boost +117 -0
- data/lib/assets/javascripts/booster/view.js.boost +45 -0
- data/lib/assets/javascripts/booster/views/composite.js.boost +59 -0
- data/lib/assets/javascripts/booster/views/layout.js.boost +94 -0
- data/lib/booster.rb +7 -0
- data/lib/booster/engine.rb +8 -0
- data/lib/booster/handlebars.rb +50 -0
- data/lib/booster/template.rb +59 -0
- data/lib/booster/version.rb +3 -0
- data/test/booster/tilt_test.rb +35 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js.boost +18 -0
- data/test/dummy/app/assets/javascripts/booster/support/helpers_spec.js.boost +69 -0
- data/test/dummy/app/assets/javascripts/booster/support/i18n_spec.js.boost +0 -0
- data/test/dummy/app/assets/javascripts/booster/support/observer_spec.js.boost +56 -0
- data/test/dummy/app/assets/javascripts/booster/support/router_spec.js.boost +19 -0
- data/test/dummy/app/assets/javascripts/booster/support/schema_spec.js.boost +79 -0
- data/test/dummy/app/assets/javascripts/booster/views/layout_spec.js.boost +30 -0
- data/test/dummy/app/controllers/application_controller.rb +7 -0
- data/test/dummy/app/views/layouts/application.html.erb +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +30 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +27 -0
- data/test/dummy/config/environments/production.rb +29 -0
- data/test/dummy/config/environments/test.rb +34 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/test_helper.rb +4 -0
- data/vendor/assets/javascripts/backbone.js +1158 -0
- data/vendor/assets/javascripts/handlebars-helpers.js +56 -0
- data/vendor/assets/javascripts/handlebars-vm.js +191 -0
- data/vendor/assets/javascripts/handlebars.js +1561 -0
- data/vendor/assets/javascripts/jasmine-html.js +190 -0
- data/vendor/assets/javascripts/jasmine-jquery.js +288 -0
- data/vendor/assets/javascripts/jasmine.js +2471 -0
- data/vendor/assets/javascripts/stitch.js +57 -0
- data/vendor/assets/javascripts/underscore.js +981 -0
- data/vendor/assets/stylesheets/jasmine.css +166 -0
- metadata +172 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
var observer = require('./support/observer');
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Extension of {Backbone.View} which supports the concept of entering
|
5
|
+
* and leaving a parent view -- a Composite in Booster. Leaving a composite
|
6
|
+
* means disappearing from DOM and likely involves unbinding from any events
|
7
|
+
* bound to the underlying model or collection.
|
8
|
+
*
|
9
|
+
* @extends {Backbone.View}
|
10
|
+
* @constructor
|
11
|
+
*/
|
12
|
+
|
13
|
+
exports.View = Backbone.View.extend({
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Invoked when the view has entered a parent view (a composite). The default
|
17
|
+
* behavior is to store a link to the composite for future reference.
|
18
|
+
*
|
19
|
+
* @param {Backbone.View} parent The composite view to which this view is added.
|
20
|
+
*/
|
21
|
+
|
22
|
+
enter: function(parent) {
|
23
|
+
this.parent = parent;
|
24
|
+
return this;
|
25
|
+
},
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Invoked when the view is to leave the parent composite. The default behavior is
|
29
|
+
* to remove the view from the DOM and unbind any event listeners, as well as any
|
30
|
+
* bindings to {Backbone.Events} sources.
|
31
|
+
*
|
32
|
+
* This method should not normally be invoked directly. Instead, invoke `remove` on
|
33
|
+
* the parent view which, in turn, will invoke `leave`.
|
34
|
+
*/
|
35
|
+
|
36
|
+
leave: function() {
|
37
|
+
$(this.el).remove();
|
38
|
+
this.unobserve();
|
39
|
+
this.unbind();
|
40
|
+
return this;
|
41
|
+
}
|
42
|
+
|
43
|
+
});
|
44
|
+
|
45
|
+
_.extend(exports.View.prototype, observer.mixin());
|
@@ -0,0 +1,59 @@
|
|
1
|
+
var base = require('../view');
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @extends {base.View}
|
5
|
+
* @constructor
|
6
|
+
*/
|
7
|
+
|
8
|
+
exports.View = base.View.extend({
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Adds a new child view to the composite. This does not add the view to the DOM tree,
|
12
|
+
* which needs to be handled by the inheriting view. The added view will get its
|
13
|
+
* `enter()` function invoked if it wants to register a link back to this composite.
|
14
|
+
*
|
15
|
+
* @param {base.View} view The child view to be added to the composite.
|
16
|
+
* @return {base.View} The child view that was passed in.
|
17
|
+
*/
|
18
|
+
|
19
|
+
add: function(view) {
|
20
|
+
this.children || (this.children = []);
|
21
|
+
this.children.push(view);
|
22
|
+
return view.enter(this);
|
23
|
+
},
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Removes the child view from the composite. This also removes the view from the
|
27
|
+
* DOM tree by invoking its `leave()` function, which also unbinds from any underlying
|
28
|
+
* model or collection.
|
29
|
+
*
|
30
|
+
* @param {base.View} view The child view to remove from the composite.
|
31
|
+
* @param {base.View|undefined} The view that was removed or `undefined` if it was not in the composite.
|
32
|
+
*/
|
33
|
+
|
34
|
+
remove: function(view) {
|
35
|
+
var index = this.children.indexOf(view);
|
36
|
+
if (index > -1) {
|
37
|
+
list.splice(index, 1);
|
38
|
+
delete view.parent;
|
39
|
+
return view.leave();
|
40
|
+
}
|
41
|
+
},
|
42
|
+
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Removes the composite from the parent. Overridden to recursively invoke `leave` on all
|
46
|
+
* the child views first.
|
47
|
+
*
|
48
|
+
* @override
|
49
|
+
*/
|
50
|
+
|
51
|
+
leave: function() {
|
52
|
+
_.each(this.children, function(view) {
|
53
|
+
view.leave();
|
54
|
+
});
|
55
|
+
|
56
|
+
base.View.prototype.leave.call(this);
|
57
|
+
}
|
58
|
+
|
59
|
+
});
|
@@ -0,0 +1,94 @@
|
|
1
|
+
var composite = require('./composite');
|
2
|
+
|
3
|
+
/**
|
4
|
+
* A {Backbone.View} layout that contains one or more named slots that can be yielded to
|
5
|
+
* child views of this composite view. The layout view ensures that a view in an
|
6
|
+
* area that is yielded to another view is properly cleaned up by invoking its
|
7
|
+
* `leave()` function.
|
8
|
+
*
|
9
|
+
* @extends {composite.View}
|
10
|
+
* @constructor
|
11
|
+
*/
|
12
|
+
|
13
|
+
exports.View = composite.View.extend({
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @type {String} The CSS class of the DIV element that is auto-generated for this
|
17
|
+
* view, if not overridden, and no element is specified in the options.
|
18
|
+
*/
|
19
|
+
|
20
|
+
className: 'booster-views-layout',
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Initializes the layout view by accepting a template function in the options that
|
24
|
+
* is to be used to render the view. The template function marks the available slots
|
25
|
+
* to be yielded to child views of the layout using the `data-yield` attribute.
|
26
|
+
*
|
27
|
+
* For example:
|
28
|
+
*
|
29
|
+
* <aside data-yield="sidebar"></aside>
|
30
|
+
*
|
31
|
+
* @param {Object} options The mandatory options hash containing at least the key
|
32
|
+
* `template` which is the template function to use during
|
33
|
+
* rendering, in addition to the optional {Backbone.View} options.
|
34
|
+
*/
|
35
|
+
|
36
|
+
initialize: function(options) {
|
37
|
+
this._template = options.template;
|
38
|
+
this.views = { };
|
39
|
+
this.render();
|
40
|
+
},
|
41
|
+
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Renders the layout into the auto-generated or options-supplied `this.el` using
|
45
|
+
* the template function given in the initialization options. If any child views
|
46
|
+
* are already inserted into the layout, these are added to the corresponding slot.
|
47
|
+
*/
|
48
|
+
|
49
|
+
render: function() {
|
50
|
+
$(this.el).html(this._template());
|
51
|
+
},
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Sets one or more views in the layout, notifying any existing views in the
|
55
|
+
* corresponding areas that they are about to leave, ensuring that event bindings
|
56
|
+
* are properly cleared.
|
57
|
+
*
|
58
|
+
* @param {Object.<string, Backbone.View>} views A set of views to be inserted into the
|
59
|
+
* layout, keyed by the name of the slot that
|
60
|
+
* this particular view exposes. If a slot with
|
61
|
+
* the given name cannot be found in this.el, an
|
62
|
+
* error is thrown.
|
63
|
+
*/
|
64
|
+
|
65
|
+
set: function(views) {
|
66
|
+
_.each(views, _.bind(this._insert, this));
|
67
|
+
},
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Inserts the given view into the given slot inside the layout.
|
71
|
+
*
|
72
|
+
* @param {Backbone.View} view The view to insert into the layout.
|
73
|
+
* @param {string} yield The name of the slot that should receive the view.
|
74
|
+
* @private
|
75
|
+
*/
|
76
|
+
|
77
|
+
_insert: function(view, yield) {
|
78
|
+
var slot = this.$('[data-yield="#{yield}"]'),
|
79
|
+
views = this.views;
|
80
|
+
|
81
|
+
if (slot.length === 0) {
|
82
|
+
throw new Error('The layout template does not contain any slot named #{yield}');
|
83
|
+
}
|
84
|
+
|
85
|
+
if (views[yield]) {
|
86
|
+
this.remove(views[yield]);
|
87
|
+
}
|
88
|
+
|
89
|
+
slot.html(view.el);
|
90
|
+
view.enter(this)
|
91
|
+
views[yield] = this.add(view);
|
92
|
+
}
|
93
|
+
|
94
|
+
});
|
data/lib/booster.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Based on https://github.com/josh/ruby-coffee-script
|
2
|
+
require 'execjs'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Booster
|
6
|
+
|
7
|
+
# Support class for compiling Handlebars templates to JavaScript
|
8
|
+
# source code using the Handlebars.js library with ExecJS.
|
9
|
+
class Handlebars
|
10
|
+
class << self
|
11
|
+
def precompile(*args)
|
12
|
+
context.call('Handlebars.precompile', *args)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def context
|
18
|
+
@context ||= ExecJS.compile(source)
|
19
|
+
end
|
20
|
+
|
21
|
+
def source
|
22
|
+
@source ||= path.read + patched_source
|
23
|
+
end
|
24
|
+
|
25
|
+
# Renames the built-in nameLookup method to make Handlebars.js support name lookups not
|
26
|
+
# only directly on an object (object.title), but also via the `Backbone.Model#get()` which
|
27
|
+
# is the way Backbone.Model exposes data attributes for a particular model.
|
28
|
+
#
|
29
|
+
# `Hello {{name}}` will then try `context.name` and then `context.get('name')`
|
30
|
+
# if `context.name` returns `undefined`.
|
31
|
+
def patched_source
|
32
|
+
<<-PATCHED_SOURCE
|
33
|
+
Handlebars.JavaScriptCompiler.prototype.nameLookupOriginal = Handlebars.JavaScriptCompiler.prototype.nameLookup;
|
34
|
+
Handlebars.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
|
35
|
+
return "((" + parent + ".get ? " + parent + ".get('" + name + "') : undefined) || " +
|
36
|
+
this.nameLookupOriginal(parent, name) + ")";
|
37
|
+
}
|
38
|
+
PATCHED_SOURCE
|
39
|
+
end
|
40
|
+
|
41
|
+
def path
|
42
|
+
@path ||= assets_path.join('javascripts', 'handlebars.js')
|
43
|
+
end
|
44
|
+
|
45
|
+
def assets_path
|
46
|
+
@assets_path ||= Pathname(__FILE__).dirname.join('..','..','vendor','assets')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'tilt'
|
2
|
+
|
3
|
+
module Booster
|
4
|
+
|
5
|
+
# Tilt integration which pre-processes `.booster` files by wrapping them in a
|
6
|
+
# CommonJS-like closure and converts inline templates to JavaScript functions
|
7
|
+
# using the Handlebars compiler. While it is at it, it also does string interpolation
|
8
|
+
# on regular JavaScript strings.
|
9
|
+
class Template < Tilt::Template
|
10
|
+
|
11
|
+
# Regex for capturing sections of template code to be compiled to JavaScript
|
12
|
+
TEMPLATE_SECTION = /@@\s*([A-Za-z0-9]*)\n(((?!@@).)*)/m
|
13
|
+
|
14
|
+
# Regex for capturing string interpolations
|
15
|
+
# STRING_INTERPOLATION = /('|")(.*)#\{([\s\S]+?)\}/
|
16
|
+
|
17
|
+
STRING_INTERPOLATION = /#\{([\s\S]+?)\}/
|
18
|
+
|
19
|
+
# Processing Booster files result in plain JavaScript.
|
20
|
+
def self.default_mime_type
|
21
|
+
'application/javascript'
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate(scope, locals, &block)
|
25
|
+
# Replace template code with compiled JavaScript
|
26
|
+
data.gsub!(TEMPLATE_SECTION) do
|
27
|
+
"var #{$1} = Handlebars.template(#{Handlebars.precompile($2.strip!)});\n\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert string interpolation to string concatenation with rudimentary
|
31
|
+
# guessing of quote type ($1). Since the template functions compiled in the
|
32
|
+
# previous step are just functions it is possible to use interpolations there
|
33
|
+
# as well, even if Handlebars does not support them by default. This is not
|
34
|
+
# recommended though although it works.
|
35
|
+
data.gsub!(STRING_INTERPOLATION, '\' + (\1) + \'')
|
36
|
+
|
37
|
+
# Indent the module content if in development mode for increased readability
|
38
|
+
if (Rails.env == 'development')
|
39
|
+
data.gsub!(/^(.)/, ' \1')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Wrap the whole thing in a closure and register it as a module (https://gist.github.com/1153919)
|
43
|
+
@output ||= "require.define({'#{ module_name(scope) }': function(exports, require, module) {#{data}}});\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def module_name(scope)
|
49
|
+
scope.logical_path #.split('/')[1..-1].join('/')
|
50
|
+
end
|
51
|
+
|
52
|
+
def basename(path)
|
53
|
+
path.gsub(%r{.*/}, '')
|
54
|
+
end
|
55
|
+
|
56
|
+
def prepare
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Booster
|
4
|
+
class TiltTest < Test::Unit::TestCase
|
5
|
+
def test_render
|
6
|
+
scope = Object.new
|
7
|
+
class << scope
|
8
|
+
def logical_path
|
9
|
+
'models/user'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
template = Booster::Tilt.new('/myapp/app/assets/javascripts/models/user.js.booster') {
|
14
|
+
<<-FILE
|
15
|
+
exports.Index = booster.View.extend({
|
16
|
+
greeting: 'This is Booster \#{this.version}!'
|
17
|
+
});
|
18
|
+
|
19
|
+
@@ layout
|
20
|
+
<h1 data-yield="title"></h1>
|
21
|
+
<div data-yield="content"></div>
|
22
|
+
|
23
|
+
@@ index
|
24
|
+
<ul>
|
25
|
+
{{#each models}}
|
26
|
+
<li>{{name}}</li>
|
27
|
+
{{/each}}
|
28
|
+
</ul>
|
29
|
+
FILE
|
30
|
+
}
|
31
|
+
|
32
|
+
puts template.render(scope, {})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
3
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
4
|
+
|
5
|
+
require File.expand_path('../config/application', __FILE__)
|
6
|
+
|
7
|
+
Dummy::Application.load_tasks
|
@@ -0,0 +1,18 @@
|
|
1
|
+
//= require jquery
|
2
|
+
//= require booster
|
3
|
+
//= require_tree .
|
4
|
+
|
5
|
+
require('booster/support/schema_spec');
|
6
|
+
require('booster/support/router_spec');
|
7
|
+
require('booster/support/helpers_spec');
|
8
|
+
require('booster/support/observer_spec');
|
9
|
+
require('booster/views/layout_spec');
|
10
|
+
|
11
|
+
describe('inline templates', function() {
|
12
|
+
it('should compile inline templates to JavaScript functions', function() {
|
13
|
+
expect(greeting({message: 'Too weird?'})).toEqual('<h1>Too weird?</h1>');
|
14
|
+
});
|
15
|
+
});
|
16
|
+
|
17
|
+
@@ greeting
|
18
|
+
<h1>{{message}}</h1>
|
@@ -0,0 +1,69 @@
|
|
1
|
+
var helpers = require('booster/support/helpers');
|
2
|
+
|
3
|
+
describe('helpers', function() {
|
4
|
+
var Model = Backbone.Model.extend({
|
5
|
+
statusOptionsObject: {
|
6
|
+
7: 'Lucky',
|
7
|
+
42: 'Meaningful'
|
8
|
+
},
|
9
|
+
statusOptionsArray: [7, 42],
|
10
|
+
});
|
11
|
+
|
12
|
+
beforeEach(function() {
|
13
|
+
this.model = new Model({
|
14
|
+
title: 'Boyachaka',
|
15
|
+
status: 42
|
16
|
+
});
|
17
|
+
})
|
18
|
+
|
19
|
+
it('should include a form input helper', function() {
|
20
|
+
expect(input(this.model)).toEqual(
|
21
|
+
'<input class="test" data-cid="#{this.model.cid}" name="title" value="Boyachaka"></input>'
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
it('should include a form textarea helper', function() {
|
26
|
+
expect(textarea(this.model)).toEqual(
|
27
|
+
'<textarea class="test" data-cid="#{this.model.cid}" name="title">Boyachaka</textarea>'
|
28
|
+
);
|
29
|
+
});
|
30
|
+
|
31
|
+
it('should include a form radio button helper', function() {
|
32
|
+
expect(radio(this.model)).toEqual(
|
33
|
+
'<input class="test" name="status" value="7" type="radio"></input>\n' +
|
34
|
+
' <input class="test" name="status" value="42" type="radio" checked="true"></input>'
|
35
|
+
);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('should include a form select helper', function() {
|
39
|
+
expect(selectObject(this.model)).toEqual(
|
40
|
+
'<select class="test" data-cid="#{this.model.cid}" name="status">\n' +
|
41
|
+
'<option value="7" >Lucky</option>\n' +
|
42
|
+
'<option value="42" selected>Meaningful</option>\n' +
|
43
|
+
'</select>'
|
44
|
+
);
|
45
|
+
|
46
|
+
expect(selectArray(this.model)).toEqual(
|
47
|
+
'<select class="test" data-cid="#{this.model.cid}" name="status">\n' +
|
48
|
+
'<option value="7" >7</option>\n' +
|
49
|
+
'<option value="42" selected>42</option>\n' +
|
50
|
+
'</select>'
|
51
|
+
);
|
52
|
+
});
|
53
|
+
})
|
54
|
+
|
55
|
+
@@ input
|
56
|
+
{{input "title" class="test" data-cid=cid}}
|
57
|
+
|
58
|
+
@@ textarea
|
59
|
+
{{textarea "title" class="test" data-cid=cid}}
|
60
|
+
|
61
|
+
@@ radio
|
62
|
+
{{radio "status" 7 class="test"}}
|
63
|
+
{{radio "status" 42 class="test"}}
|
64
|
+
|
65
|
+
@@ selectObject
|
66
|
+
{{select "status" statusOptionsObject class="test" data-cid=cid}}
|
67
|
+
|
68
|
+
@@ selectArray
|
69
|
+
{{select "status" statusOptionsArray class="test" data-cid=cid}}
|