fifty 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +186 -0
- data/lib/fifty/helpers.rb +92 -0
- data/lib/fifty.rb +198 -0
- metadata +128 -0
data/README.md
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
## Fifty
|
2
|
+
|
3
|
+
Have you ever written the same template or helper twice? Do you have trouble sleeping at night because of code duplication? Then Fifty may right for you. It allows you to save keystrokes by leveraging:
|
4
|
+
|
5
|
+
- HAML as a markup language for the static part of your templates.
|
6
|
+
- Handlebars to render dynamic fields on the server or in the browser.
|
7
|
+
- Javascript to write helpers that run both client- and server-side.
|
8
|
+
|
9
|
+
## Is it ready?
|
10
|
+
|
11
|
+
No. The proof of concept is there for you to see, but I need to work on this some more before I release it as a gem.
|
12
|
+
|
13
|
+
## What does it do?
|
14
|
+
|
15
|
+
Hopefully this is self-explanatory:
|
16
|
+
|
17
|
+
### 1. Start with some model data
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
$data = {
|
21
|
+
posts: [
|
22
|
+
{ title: 'Hello, world!',
|
23
|
+
text: 'This is fu-man-chu!',
|
24
|
+
likes: 2,
|
25
|
+
comments: [
|
26
|
+
{ user: 'Louis',
|
27
|
+
text: 'Cool!' },
|
28
|
+
{ user: 'Justin',
|
29
|
+
text: 'What do you think?' }
|
30
|
+
] },
|
31
|
+
{ title: 'This is a post!',
|
32
|
+
text: 'With lots of love.',
|
33
|
+
likes: 1,
|
34
|
+
comments: [
|
35
|
+
{ user: 'Chris',
|
36
|
+
text: 'Nice!' }
|
37
|
+
] }
|
38
|
+
]
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
### 2. Include Fifty in our app.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'sinatra'
|
46
|
+
require 'fifty'
|
47
|
+
|
48
|
+
class MyApp < Sinatra::Base
|
49
|
+
|
50
|
+
helpers Fifty
|
51
|
+
|
52
|
+
# Share a copy of the data to the client.
|
53
|
+
# Made available as `window.client`
|
54
|
+
shared_data :client_data, $data.to_json
|
55
|
+
|
56
|
+
get '/posts?.format?' do |format|
|
57
|
+
if format == 'json'
|
58
|
+
$data.to_json
|
59
|
+
elsif format == 'html'
|
60
|
+
fifty :posts, $data
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
MyApp.run!
|
67
|
+
|
68
|
+
```
|
69
|
+
|
70
|
+
### 3. Define your views
|
71
|
+
|
72
|
+
|
73
|
+
```haml
|
74
|
+
@@ layout
|
75
|
+
|
76
|
+
%html
|
77
|
+
%head
|
78
|
+
%script{src: fifty_cdn(:handlebars)}
|
79
|
+
%script{src: fifty_cdn(:jquery)}
|
80
|
+
|
81
|
+
= fifty_partials
|
82
|
+
= fifty_scripts
|
83
|
+
|
84
|
+
%body
|
85
|
+
#posts
|
86
|
+
= yield
|
87
|
+
|
88
|
+
:javascript
|
89
|
+
|
90
|
+
// Reload from route /posts.json
|
91
|
+
$('#reload').click(function () {
|
92
|
+
$('#posts').getAndReplace('posts');
|
93
|
+
});
|
94
|
+
|
95
|
+
// Submit form and add post to feed.
|
96
|
+
$('#form').submit(function (event) {
|
97
|
+
event.preventDefault();
|
98
|
+
var data = $(this).serialize;
|
99
|
+
$('#posts').postAndAppend('post', data);
|
100
|
+
});
|
101
|
+
|
102
|
+
// Delete post and remove from feed.
|
103
|
+
$('.delete').click(function (e) {
|
104
|
+
$(this).postAndRemove('post/delete', {
|
105
|
+
id: $(this).closest('.post')
|
106
|
+
});
|
107
|
+
});
|
108
|
+
|
109
|
+
@@ posts
|
110
|
+
|
111
|
+
.container
|
112
|
+
.row
|
113
|
+
{{#each posts}}
|
114
|
+
{{> post}}
|
115
|
+
{{/each}}
|
116
|
+
|
117
|
+
%button#reload Reload
|
118
|
+
|
119
|
+
@@ post
|
120
|
+
|
121
|
+
.post{id: "{{id}}"}
|
122
|
+
%h3 {{title}}
|
123
|
+
%p {{text}}
|
124
|
+
%p
|
125
|
+
{{likes}}
|
126
|
+
{{t "like"}}
|
127
|
+
%hr
|
128
|
+
%p
|
129
|
+
{{#each comments}}
|
130
|
+
{{> comment}}
|
131
|
+
{{/each}}
|
132
|
+
|
133
|
+
%a.delete{href: '#'} Delete
|
134
|
+
|
135
|
+
@@ comment
|
136
|
+
|
137
|
+
.comment{id: "{{id}}"}
|
138
|
+
%b {{user}}:
|
139
|
+
%span {{text}}
|
140
|
+
|
141
|
+
@@ form
|
142
|
+
|
143
|
+
%form{method: 'post'}
|
144
|
+
Name:
|
145
|
+
%input#name{type: 'text'}
|
146
|
+
Message:
|
147
|
+
%textarea#message
|
148
|
+
|
149
|
+
```
|
150
|
+
|
151
|
+
## Configuration Options
|
152
|
+
|
153
|
+
You can put these anywhere before you run your routes. For a Sinatra app, the canonical path is `config/fifty.rb`.
|
154
|
+
|
155
|
+
**Paths to Views and Locales**
|
156
|
+
|
157
|
+
Set the path(s) to your views (shown is the default):
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
Fifty.views = ['./views/**/*.haml']
|
161
|
+
```
|
162
|
+
|
163
|
+
If using locales with `I18n`, set the path to your files:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
Fifty.locales = ['./config/locales/*.yml']
|
167
|
+
```
|
168
|
+
|
169
|
+
**Shared Helpers and JSON data**
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# Share a copy of the data to the client.
|
173
|
+
#
|
174
|
+
# Made available as `window.client`
|
175
|
+
|
176
|
+
js_data :client_data, $data.to_json
|
177
|
+
|
178
|
+
# Define a shared helper function.
|
179
|
+
#
|
180
|
+
# Can be invoked within a HBS
|
181
|
+
# template in Ruby or Javascript.
|
182
|
+
|
183
|
+
hbs_helper :add, %t{
|
184
|
+
function (a, b) { return a + b; }
|
185
|
+
}
|
186
|
+
```
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Fifty::Helpers
|
2
|
+
|
3
|
+
module Locales
|
4
|
+
|
5
|
+
require 'i18n'
|
6
|
+
require 'yaml'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
Fifty.helper :t, "function(element) { return locales[locale][element]; }"
|
10
|
+
|
11
|
+
Fifty.class_eval do
|
12
|
+
class << self
|
13
|
+
attr_accessor :locales
|
14
|
+
end
|
15
|
+
self.locales = ['./config/locales/*.yml']
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.compile
|
19
|
+
locales = Fifty.locales.map { |p| Dir[p] }.flatten
|
20
|
+
hash = {}
|
21
|
+
locales.each do |file|
|
22
|
+
name = File.basename(file, '.yml')
|
23
|
+
yaml = YAML.load(File.read(file))
|
24
|
+
hash.merge!(yaml)
|
25
|
+
end
|
26
|
+
Fifty.shared :locale, "'#{I18n.locale.to_s}'"
|
27
|
+
Fifty.shared :locales, hash.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module Render
|
33
|
+
|
34
|
+
Fifty.shared :Fifty, "{
|
35
|
+
|
36
|
+
render: function(template, data) {
|
37
|
+
var hbs = $('#_' + template).html();
|
38
|
+
var tmpl = Handlebars.compile(hbs);
|
39
|
+
return tmpl(data);
|
40
|
+
},
|
41
|
+
|
42
|
+
getAndRender: function(name, params) {
|
43
|
+
$.getJSON('/' + name + '.json',
|
44
|
+
function (data) {
|
45
|
+
Fifty.render(name, data);
|
46
|
+
}, $.param(params));
|
47
|
+
},
|
48
|
+
|
49
|
+
postAndRender: function(name, params) {
|
50
|
+
$.post('/' + name + '.json', $.param(params),
|
51
|
+
function (data) {
|
52
|
+
Fifty.render(name, data);
|
53
|
+
});
|
54
|
+
},
|
55
|
+
|
56
|
+
replace: function(id, name, data) {
|
57
|
+
$(id).html(Fifty.render(name, data));
|
58
|
+
},
|
59
|
+
|
60
|
+
getAndReplace: function(id, name, params) {
|
61
|
+
$(id).html(Fifty.getAndRender(name, params));
|
62
|
+
},
|
63
|
+
|
64
|
+
postAndReplace: function(id, name, params) {
|
65
|
+
$(id).html(Fifty.postAndRender(name, params));
|
66
|
+
},
|
67
|
+
|
68
|
+
append: function(id, name, data) {
|
69
|
+
$(id).append(Fifty.render(name, data));
|
70
|
+
},
|
71
|
+
|
72
|
+
getAndAppend: function(id, name, params) {
|
73
|
+
$(id).append(Fifty.getAndRender(name, params));
|
74
|
+
},
|
75
|
+
|
76
|
+
postAndAppend: function(id, name, params) {
|
77
|
+
$(id).append(Fifty.postAndRender(name, params));
|
78
|
+
},
|
79
|
+
|
80
|
+
postAndRemove: function(id, name, params) {
|
81
|
+
$.post('/' + name + '.json', $.param(param),
|
82
|
+
function () {
|
83
|
+
$(id).remove();
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
}"
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
end
|
data/lib/fifty.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
module Fifty
|
2
|
+
|
3
|
+
require 'handlebars'
|
4
|
+
require 'haml'
|
5
|
+
require 'binding_of_caller'
|
6
|
+
|
7
|
+
VERSION = '0.0.1'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :views
|
11
|
+
attr_accessor :compiled
|
12
|
+
end
|
13
|
+
|
14
|
+
self.views = ['./views/**/*.haml']
|
15
|
+
self.compiled = false
|
16
|
+
|
17
|
+
def self.cdn(name)
|
18
|
+
if name == :handlebars
|
19
|
+
'https://cdnjs.cloudflare.com/ajax/libs/' +
|
20
|
+
'handlebars.js/1.0.0-rc.3/handlebars.min.js'
|
21
|
+
elsif name == :jquery
|
22
|
+
'https://cdnjs.cloudflare.com/' +
|
23
|
+
'ajax/libs/jquery/1.9.1/jquery.min.js'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@@handlebars = Handlebars::Context.new
|
28
|
+
@@helpers, @@shared, @@partials = {}, {}, {}
|
29
|
+
@@view_paths = []
|
30
|
+
|
31
|
+
def self.helper(name, fn)
|
32
|
+
code = self.register_helper(name, fn)
|
33
|
+
@@helpers[name] = code
|
34
|
+
self.eval_js(code)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.shared(name, code)
|
38
|
+
code = "var #{name} = #{code};\n"
|
39
|
+
@@shared[name] = code
|
40
|
+
self.eval_js(code)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.compile
|
44
|
+
self.compile_statics
|
45
|
+
self.compile_inlines
|
46
|
+
self.compiled = true
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.compile_statics
|
50
|
+
views = self.views.map { |p| Dir[p] }.flatten
|
51
|
+
context = binding.of_caller(3)
|
52
|
+
views.each do |file|
|
53
|
+
path = File.dirname(file)
|
54
|
+
unless @@view_paths.include?(path)
|
55
|
+
@@view_paths << path
|
56
|
+
end
|
57
|
+
template = File.read(file)
|
58
|
+
name = '_' + File.basename(file, '.haml')
|
59
|
+
next if name == '_layout'
|
60
|
+
partial = Haml::Engine.
|
61
|
+
new(template).render(
|
62
|
+
context, layout: false)
|
63
|
+
partial = self.escape_hbs(partial)
|
64
|
+
@@partials[name] = partial
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.compile_inlines
|
69
|
+
inlines = Sinatra::Application.templates
|
70
|
+
context = binding.of_caller(3)
|
71
|
+
inlines.each do |name, info|
|
72
|
+
next if name == :layout
|
73
|
+
partial = Haml::Engine.
|
74
|
+
new(info[0].strip).render(context)
|
75
|
+
partial = self.escape_hbs(partial)
|
76
|
+
@@partials[name] = partial
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.register_helper(name, fn)
|
81
|
+
"Handlebars.registerHelper('#{name}', #{fn});"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.eval_js(code)
|
85
|
+
@@handlebars.instance_eval do
|
86
|
+
@js.runtime.eval(code)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.escape_hbs(partial)
|
91
|
+
2.times do
|
92
|
+
partial.gsub!("{{", "{%{")
|
93
|
+
partial.gsub!("}}", "}%}")
|
94
|
+
end
|
95
|
+
partial
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.included(base)
|
99
|
+
base.class_eval {
|
100
|
+
Fifty::Helpers::Locales.compile }
|
101
|
+
end
|
102
|
+
|
103
|
+
def fifty(view, data)
|
104
|
+
Fifty.compile unless Fifty.compiled
|
105
|
+
hbs2html(haml2hbs(view), data)
|
106
|
+
end
|
107
|
+
|
108
|
+
alias :fu :fifty
|
109
|
+
|
110
|
+
def self.partials
|
111
|
+
partials_hbs = ''
|
112
|
+
@@partials.each do |name, code|
|
113
|
+
partials_hbs += script_tag(name,
|
114
|
+
code, 'x-text-handlebars')
|
115
|
+
end
|
116
|
+
partials_js = ''
|
117
|
+
@@partials.each do |name, code|
|
118
|
+
partials_js += register_partial(name) + "\n"
|
119
|
+
end
|
120
|
+
partials_js = script_tag('partials', partials_js)
|
121
|
+
[partials_hbs, partials_js].join
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.scripts
|
125
|
+
scripts_js = ''
|
126
|
+
@@helpers.each do |name, code|
|
127
|
+
scripts_js += code + "\n"
|
128
|
+
end
|
129
|
+
scripts_js = script_tag('helpers', scripts_js)
|
130
|
+
shared_js = "\n"
|
131
|
+
@@shared.each do |name, code|
|
132
|
+
shared_js += code + "\n"
|
133
|
+
end
|
134
|
+
shared_js = script_tag('shared', shared_js)
|
135
|
+
[scripts_js, shared_js].join
|
136
|
+
end
|
137
|
+
|
138
|
+
require_relative 'fifty/helpers'
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def haml2hbs(name)
|
143
|
+
context = binding.of_caller(4)
|
144
|
+
result = render_haml(name, context, layout: false)
|
145
|
+
while data = result.match(/{{> ([^}]*)}}/)
|
146
|
+
partial = render_haml($1, context, layout: false)
|
147
|
+
result = result.gsub(data[0], partial)
|
148
|
+
end
|
149
|
+
render_layout(context, result)
|
150
|
+
end
|
151
|
+
|
152
|
+
def render_layout(context, insert)
|
153
|
+
render_haml(:layout, context) do
|
154
|
+
insert
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_template(name)
|
159
|
+
inlines = Sinatra::Application.templates
|
160
|
+
if path = find_haml_template(name)
|
161
|
+
File.read(path)
|
162
|
+
elsif partial = inlines[name.intern]
|
163
|
+
partial.first
|
164
|
+
else
|
165
|
+
raise "No template for #{name}."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def find_haml_template(name)
|
170
|
+
@@view_paths.each do |path|
|
171
|
+
file = File.join(path, name.to_s + '.haml')
|
172
|
+
return file if File.readable?(file)
|
173
|
+
end
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
def render_haml(name, context = nil, options = {}, &block)
|
178
|
+
Haml::Engine.new(get_template(name), options).render(context, &block)
|
179
|
+
end
|
180
|
+
|
181
|
+
def hbs2html(html, data)
|
182
|
+
unescape_hbs(@@handlebars.compile(html).call(data))
|
183
|
+
end
|
184
|
+
|
185
|
+
def unescape_hbs(html)
|
186
|
+
html.gsub("{%{", "{{").gsub("}%}", "}}")
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.script_tag(id, code, type = 'text/javascript')
|
190
|
+
"\n<script id='#{id}' type='#{type}'>#{code}</script>\n"
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.register_partial(name)
|
194
|
+
"Handlebars.registerPartial('#{name[1..-1]}'," +
|
195
|
+
" document.getElementById('#{name}').innerText);"
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fifty
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Louis Mullie
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: haml
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: handlebars
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: i18n
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: binding_of_caller
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: ''
|
95
|
+
email:
|
96
|
+
- louis.mullie@gmail.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- README.md
|
102
|
+
- lib/fifty/helpers.rb
|
103
|
+
- lib/fifty.rb
|
104
|
+
homepage: https://github.com/louismullie/fifty
|
105
|
+
licenses: []
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 1.8.25
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: ''
|
128
|
+
test_files: []
|