booster 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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}}
|