fifty 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +186 -0
  2. data/lib/fifty/helpers.rb +92 -0
  3. data/lib/fifty.rb +198 -0
  4. 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: []