curly-templates 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.md +125 -0
- data/Rakefile +132 -0
- data/curly-templates.gemspec +44 -0
- data/lib/curly.rb +67 -0
- data/lib/curly/presenter.rb +80 -0
- data/lib/curly/railtie.rb +8 -0
- data/lib/curly/template_handler.rb +51 -0
- data/spec/curly_spec.rb +92 -0
- data/spec/presenter_spec.rb +29 -0
- data/spec/spec_helper.rb +0 -0
- data/spec/template_handler_spec.rb +161 -0
- metadata +111 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
Curly
|
2
|
+
=======
|
3
|
+
|
4
|
+
Free your views!
|
5
|
+
|
6
|
+
Curly is a template language that completely separates structure and logic.
|
7
|
+
Instead of interspersing your HTML with snippets of Ruby, all logic is moved
|
8
|
+
to a presenter class, with only simple placeholders in the HTML.
|
9
|
+
|
10
|
+
While the basic concepts are very similar to [Mustache](http://mustache.github.com/)
|
11
|
+
or [Handlebars](http://handlebarsjs.com/), Curly is different in some key ways:
|
12
|
+
|
13
|
+
- Instead of the template controlling the variable scope and looping through
|
14
|
+
data, all logic is left to the presenter object. This means that untrusted
|
15
|
+
templates can safely be executed, making Curly a possible alternative to
|
16
|
+
languages like [Liquid](http://liquidmarkup.org/).
|
17
|
+
- Instead of implementing its own template resolution mechanism, Curly hooks
|
18
|
+
directly into Rails, leveraging the existing resolvers.
|
19
|
+
- Because of the way it integrates with Rails, it is very easy to use partial
|
20
|
+
Curly templates to split out logic from a presenter. With Mustache, at least,
|
21
|
+
when integrating with Rails, it is common to return Hash objects from view
|
22
|
+
object methods that are in turn used by the template.
|
23
|
+
|
24
|
+
|
25
|
+
Examples
|
26
|
+
--------
|
27
|
+
|
28
|
+
Here is a simple Curly template -- it will be looked up by Rails automatically.
|
29
|
+
|
30
|
+
```html
|
31
|
+
<!-- app/views/posts/show.html.curly -->
|
32
|
+
<h1>{{title}}<h1>
|
33
|
+
<p class="author">{{author}}</p>
|
34
|
+
<p>{{description}}</p>
|
35
|
+
|
36
|
+
{{comment_form}}
|
37
|
+
|
38
|
+
<div class="comments">
|
39
|
+
{{comments}}
|
40
|
+
</div>
|
41
|
+
```
|
42
|
+
|
43
|
+
When rendering the template, a presenter is automatically instantiated with the
|
44
|
+
variables assigned in the controller or the `render` call. The presenter declares
|
45
|
+
the variables it expects with `presents`, which takes a list of variables names.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# app/presenters/posts/show_presenter.rb
|
49
|
+
class Posts::ShowPresenter < Curly::Presenter
|
50
|
+
presents :post
|
51
|
+
|
52
|
+
def title
|
53
|
+
@post.title
|
54
|
+
end
|
55
|
+
|
56
|
+
def author
|
57
|
+
link_to(@post.author.name, @post.author, rel: "author")
|
58
|
+
end
|
59
|
+
|
60
|
+
def description
|
61
|
+
Markdown.new(@post.description).to_html.html_safe
|
62
|
+
end
|
63
|
+
|
64
|
+
def comments
|
65
|
+
render 'comment', collection: @post.comments
|
66
|
+
end
|
67
|
+
|
68
|
+
def comment_form
|
69
|
+
if @post.comments_allowed?
|
70
|
+
render 'comment_form', post: @post
|
71
|
+
else
|
72
|
+
content_tag(:p, "Comments are disabled for this post")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
|
79
|
+
Caching
|
80
|
+
-------
|
81
|
+
|
82
|
+
Because of the way logic is contained in presenters, caching entire views or partials
|
83
|
+
becomes exceedingly straightforward. Simply define a `#cache_key` method that returns
|
84
|
+
a non-nil object, and the return value will be used to cache the template.
|
85
|
+
|
86
|
+
Whereas in ERB your would include the `cache` call in the template itself:
|
87
|
+
|
88
|
+
```erb
|
89
|
+
<% cache([@post, signed_in?]) do %>
|
90
|
+
...
|
91
|
+
<% end %>
|
92
|
+
```
|
93
|
+
|
94
|
+
In Curly you would instead declare it in the presenter:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class Posts::ShowPresenter < Curly::Presenter
|
98
|
+
presents :post
|
99
|
+
|
100
|
+
def cache_key
|
101
|
+
[@post, signed_in?]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
Likewise, you can add a `#cache_duration` method if you wish to automatically expire
|
107
|
+
the fragment cache:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class Posts::ShowPresenter < Curly::Presenter
|
111
|
+
...
|
112
|
+
|
113
|
+
def cache_duration
|
114
|
+
30.minutes
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
|
120
|
+
Copyright and License
|
121
|
+
---------------------
|
122
|
+
|
123
|
+
Copyright (c) 2013 Daniel Schierbeck (@dasch), Zendesk Inc.
|
124
|
+
|
125
|
+
Licensed under the [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
data/Rakefile
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'rake'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
Bundler.setup
|
7
|
+
|
8
|
+
#############################################################################
|
9
|
+
#
|
10
|
+
# Helper functions
|
11
|
+
#
|
12
|
+
#############################################################################
|
13
|
+
|
14
|
+
def name
|
15
|
+
"curly"
|
16
|
+
end
|
17
|
+
|
18
|
+
def gem_name
|
19
|
+
"#{name}-templates"
|
20
|
+
end
|
21
|
+
|
22
|
+
def version
|
23
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
24
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def date
|
28
|
+
Date.today.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def gemspec_file
|
32
|
+
"#{gem_name}.gemspec"
|
33
|
+
end
|
34
|
+
|
35
|
+
def gem_file
|
36
|
+
"#{gem_name}-#{version}.gem"
|
37
|
+
end
|
38
|
+
|
39
|
+
def replace_header(head, header_name, value = nil)
|
40
|
+
value ||= send(header_name)
|
41
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{value}'"}
|
42
|
+
end
|
43
|
+
|
44
|
+
#############################################################################
|
45
|
+
#
|
46
|
+
# Standard tasks
|
47
|
+
#
|
48
|
+
#############################################################################
|
49
|
+
|
50
|
+
task :default => :spec
|
51
|
+
|
52
|
+
require 'rspec/core/rake_task'
|
53
|
+
RSpec::Core::RakeTask.new(:spec)
|
54
|
+
|
55
|
+
require 'rdoc/task'
|
56
|
+
Rake::RDocTask.new do |rdoc|
|
57
|
+
rdoc.rdoc_dir = 'rdoc'
|
58
|
+
rdoc.title = "#{name} #{version}"
|
59
|
+
rdoc.rdoc_files.include('README*')
|
60
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Open an irb session preloaded with this library"
|
64
|
+
task :console do
|
65
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
66
|
+
end
|
67
|
+
|
68
|
+
#############################################################################
|
69
|
+
#
|
70
|
+
# Packaging tasks
|
71
|
+
#
|
72
|
+
#############################################################################
|
73
|
+
|
74
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
75
|
+
task :release => :build do
|
76
|
+
unless `git branch` =~ /^\* master$/
|
77
|
+
puts "You must be on the master branch to release!"
|
78
|
+
exit!
|
79
|
+
end
|
80
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
81
|
+
sh "git tag v#{version}"
|
82
|
+
sh "git push origin master"
|
83
|
+
sh "git push origin v#{version}"
|
84
|
+
sh "gem push pkg/#{gem_name}-#{version}.gem"
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "Build #{gem_file} into the pkg directory"
|
88
|
+
task :build => :gemspec do
|
89
|
+
sh "mkdir -p pkg"
|
90
|
+
sh "gem build #{gemspec_file}"
|
91
|
+
sh "mv #{gem_file} pkg"
|
92
|
+
end
|
93
|
+
|
94
|
+
desc "Generate #{gemspec_file}"
|
95
|
+
task :gemspec => :validate do
|
96
|
+
# read spec file and split out manifest section
|
97
|
+
spec = File.read(gemspec_file)
|
98
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
99
|
+
|
100
|
+
# replace name version and date
|
101
|
+
replace_header(head, :name, gem_name)
|
102
|
+
replace_header(head, :version)
|
103
|
+
replace_header(head, :date)
|
104
|
+
|
105
|
+
# determine file list from git ls-files
|
106
|
+
files = `git ls-files`.
|
107
|
+
split("\n").
|
108
|
+
sort.
|
109
|
+
reject { |file| file =~ /^\./ }.
|
110
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
111
|
+
map { |file| " #{file}" }.
|
112
|
+
join("\n")
|
113
|
+
|
114
|
+
# piece file back together and write
|
115
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
116
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
117
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
118
|
+
puts "Updated #{gemspec_file}"
|
119
|
+
end
|
120
|
+
|
121
|
+
desc "Validate #{gemspec_file}"
|
122
|
+
task :validate do
|
123
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
124
|
+
unless libfiles.empty?
|
125
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
126
|
+
exit!
|
127
|
+
end
|
128
|
+
unless Dir['VERSION*'].empty?
|
129
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
130
|
+
exit!
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
|
+
s.rubygems_version = '1.3.5'
|
5
|
+
|
6
|
+
s.name = 'curly-templates'
|
7
|
+
s.version = '0.1.0'
|
8
|
+
s.date = '2013-01-21'
|
9
|
+
|
10
|
+
s.summary = "Free your views!"
|
11
|
+
s.description = "A view layer for your Rails apps that separates structure and logic."
|
12
|
+
|
13
|
+
s.authors = ["Daniel Schierbeck"]
|
14
|
+
s.email = 'dasch@zendesk.com'
|
15
|
+
s.homepage = 'http://github.com/zendesk/curly-templates'
|
16
|
+
|
17
|
+
s.require_paths = %w[lib]
|
18
|
+
|
19
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
20
|
+
|
21
|
+
s.add_dependency("actionpack", "~> 3.2.11")
|
22
|
+
|
23
|
+
s.add_development_dependency("rake")
|
24
|
+
s.add_development_dependency("rspec", "~> 2.12.0")
|
25
|
+
|
26
|
+
# = MANIFEST =
|
27
|
+
s.files = %w[
|
28
|
+
Gemfile
|
29
|
+
README.md
|
30
|
+
Rakefile
|
31
|
+
curly-templates.gemspec
|
32
|
+
lib/curly.rb
|
33
|
+
lib/curly/presenter.rb
|
34
|
+
lib/curly/railtie.rb
|
35
|
+
lib/curly/template_handler.rb
|
36
|
+
spec/curly_spec.rb
|
37
|
+
spec/presenter_spec.rb
|
38
|
+
spec/spec_helper.rb
|
39
|
+
spec/template_handler_spec.rb
|
40
|
+
]
|
41
|
+
# = MANIFEST =
|
42
|
+
|
43
|
+
s.test_files = s.files.select { |path| path =~ /^spec\/.*_spec\.rb/ }
|
44
|
+
end
|
data/lib/curly.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Curly is a simple view system. Each view consists of two parts, a
|
2
|
+
# template and a presenter. The template is a simple string that can contain
|
3
|
+
# references in the format `{{refname}}`, e.g.
|
4
|
+
#
|
5
|
+
# Hello {{recipient}},
|
6
|
+
# you owe us ${{amount}}.
|
7
|
+
#
|
8
|
+
# The references will be converted into messages that are sent to the
|
9
|
+
# presenter, which is any Ruby object. Only public methods can be referenced.
|
10
|
+
# To continue the earlier example, here's the matching presenter:
|
11
|
+
#
|
12
|
+
# class BankPresenter
|
13
|
+
# def initialize(recipient, amount)
|
14
|
+
# @recipient, @amount = recipient, amount
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def recipient
|
18
|
+
# @recipient.full_name
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def amount
|
22
|
+
# "%.2f" % @amount
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# See Curly::Presenter for more information on presenters.
|
27
|
+
#
|
28
|
+
module Curly
|
29
|
+
VERSION = "0.1.0"
|
30
|
+
|
31
|
+
REFERENCE_REGEX = %r(\{\{(\w+)\}\})
|
32
|
+
|
33
|
+
class InvalidReference < StandardError
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.compile(template)
|
37
|
+
source = template.inspect
|
38
|
+
source.gsub!(REFERENCE_REGEX) { compile_reference($1) }
|
39
|
+
|
40
|
+
source
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.valid?(template, presenter_class)
|
44
|
+
references = extract_references(template)
|
45
|
+
methods = presenter_class.available_methods.map(&:to_s)
|
46
|
+
references & methods == references
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def self.compile_reference(reference)
|
52
|
+
%(\#{
|
53
|
+
if presenter.method_available?(:#{reference})
|
54
|
+
result = presenter.#{reference} {|*args| yield(*args) }
|
55
|
+
ERB::Util.html_escape(result)
|
56
|
+
else
|
57
|
+
raise Curly::InvalidReference, "invalid reference `{{#{reference}}}'"
|
58
|
+
end
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.extract_references(template)
|
63
|
+
template.scan(REFERENCE_REGEX).flatten
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
require 'curly/railtie' if defined?(Rails)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Curly
|
2
|
+
|
3
|
+
# A base class that can be subclassed by concrete presenters.
|
4
|
+
#
|
5
|
+
# A Curly presenter is responsible for delivering data to templates, in the
|
6
|
+
# form of simple strings. Each public instance method on the presenter class
|
7
|
+
# can be referenced in a template. When a template is evaluated with a
|
8
|
+
# presenter, the referenced methods will be called with no arguments, and
|
9
|
+
# the returned strings inserted in place of the references in the template.
|
10
|
+
#
|
11
|
+
# Note that strings that are not HTML safe will be escaped.
|
12
|
+
#
|
13
|
+
# A presenter is always instantiated with a context to which it delegates
|
14
|
+
# unknown messages, usually an instance of ActionView::Base provided by
|
15
|
+
# Rails. See Curly::Handler for a typical use.
|
16
|
+
#
|
17
|
+
# Examples
|
18
|
+
#
|
19
|
+
# class BlogPresenter < Curly::Presenter
|
20
|
+
# presents :post
|
21
|
+
#
|
22
|
+
# def title
|
23
|
+
# @post.title
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def body
|
27
|
+
# markdown(@post.body)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# def author
|
31
|
+
# @post.author.full_name
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# presenter = BlogPresenter.new(context, post: post)
|
36
|
+
# presenter.author #=> "Jackie Chan"
|
37
|
+
#
|
38
|
+
class Presenter
|
39
|
+
# Initializes the presenter with the given context and options.
|
40
|
+
#
|
41
|
+
# context - An ActionView::Base context.
|
42
|
+
# options - A Hash of options given to the presenter.
|
43
|
+
#
|
44
|
+
def initialize(context, options = {})
|
45
|
+
@_context = context
|
46
|
+
self.class.presented_names.each do |name|
|
47
|
+
instance_variable_set("@#{name}", options.fetch(name))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def cache_key
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def cache_duration
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_available?(method)
|
60
|
+
self.class.available_methods.include?(method)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.available_methods
|
64
|
+
public_instance_methods - Curly::Presenter.public_instance_methods
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
class_attribute :presented_names
|
70
|
+
self.presented_names = [].freeze
|
71
|
+
|
72
|
+
def self.presents(*args)
|
73
|
+
self.presented_names += args
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
@_context.public_send(method, *args, &block)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'action_view'
|
3
|
+
require 'curly'
|
4
|
+
|
5
|
+
class Curly::TemplateHandler
|
6
|
+
def self.presenter_name_for_path(path)
|
7
|
+
"#{path}_presenter".camelize
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.call(template)
|
11
|
+
presenter_class = presenter_name_for_path(template.virtual_path)
|
12
|
+
|
13
|
+
source = Curly.compile(template.source)
|
14
|
+
template_digest = Digest::MD5.hexdigest(template.source)
|
15
|
+
|
16
|
+
# Template is empty, so there's no need to initialize a presenter.
|
17
|
+
return %("") if template.source.empty?
|
18
|
+
|
19
|
+
<<-RUBY
|
20
|
+
if local_assigns.empty?
|
21
|
+
options = assigns
|
22
|
+
else
|
23
|
+
options = local_assigns
|
24
|
+
end
|
25
|
+
|
26
|
+
presenter = #{presenter_class}.new(self, options.with_indifferent_access)
|
27
|
+
|
28
|
+
view_function = lambda do
|
29
|
+
#{source}
|
30
|
+
end
|
31
|
+
|
32
|
+
if key = presenter.cache_key
|
33
|
+
@output_buffer = ActiveSupport::SafeBuffer.new
|
34
|
+
|
35
|
+
template_digest = #{template_digest.inspect}
|
36
|
+
|
37
|
+
options = {
|
38
|
+
expires_in: presenter.cache_duration
|
39
|
+
}
|
40
|
+
|
41
|
+
cache([template_digest, key], options) do
|
42
|
+
safe_concat(view_function.call)
|
43
|
+
end
|
44
|
+
|
45
|
+
@output_buffer
|
46
|
+
else
|
47
|
+
view_function.call.html_safe
|
48
|
+
end
|
49
|
+
RUBY
|
50
|
+
end
|
51
|
+
end
|
data/spec/curly_spec.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_support/core_ext/string/output_safety'
|
3
|
+
require 'curly'
|
4
|
+
|
5
|
+
describe Curly do
|
6
|
+
let :presenter_class do
|
7
|
+
Class.new do
|
8
|
+
def foo
|
9
|
+
"FOO"
|
10
|
+
end
|
11
|
+
|
12
|
+
def high_yield
|
13
|
+
"#{yield}, motherfucker!"
|
14
|
+
end
|
15
|
+
|
16
|
+
def yield_value
|
17
|
+
"#{yield :foo}, please?"
|
18
|
+
end
|
19
|
+
|
20
|
+
def unicorns
|
21
|
+
"UNICORN"
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_available?(method)
|
25
|
+
[:foo, :high_yield, :yield_value, :dirty].include?(method)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.available_methods
|
29
|
+
public_instance_methods
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def method_missing(*args)
|
35
|
+
"BAR"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:presenter) { presenter_class.new }
|
41
|
+
let(:context) { double("context", presenter: presenter) }
|
42
|
+
|
43
|
+
it "compiles Curly templates to Ruby code" do
|
44
|
+
evaluate("{{foo}}").should == "FOO"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "makes sure only public methods are called on the presenter object" do
|
48
|
+
expect { evaluate("{{bar}}") }.to raise_exception(Curly::InvalidReference)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "propagates yields to the caller" do
|
52
|
+
evaluate("{{high_yield}}") { "$$$" }.should == "$$$, motherfucker!"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "sends along arguments passed to yield" do
|
56
|
+
evaluate("{{yield_value}}") {|v| v.upcase }.should == "FOO, please?"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "escapes non HTML safe strings returned from the presenter" do
|
60
|
+
presenter.stub(:dirty) { "<p>dirty</p>" }
|
61
|
+
evaluate("{{dirty}}").should == "<p>dirty</p>"
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not escape HTML safe strings returned from the presenter" do
|
65
|
+
presenter.stub(:dirty) { "<p>dirty</p>".html_safe }
|
66
|
+
evaluate("{{dirty}}").should == "<p>dirty</p>"
|
67
|
+
end
|
68
|
+
|
69
|
+
describe ".valid?" do
|
70
|
+
it "returns true if only available methods are referenced" do
|
71
|
+
validate("Hello, {{foo}}!").should == true
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns false if a missing method is referenced" do
|
75
|
+
validate("Hello, {{i_am_missing}}").should == false
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns false if an unavailable method is referenced" do
|
79
|
+
presenter_class.stub(:available_methods) { [:foo] }
|
80
|
+
validate("Hello, {{inspect}}").should == false
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate(template)
|
84
|
+
Curly.valid?(template, presenter_class)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def evaluate(template)
|
89
|
+
code = Curly.compile(template)
|
90
|
+
context.instance_eval(code)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'curly/presenter'
|
4
|
+
|
5
|
+
describe Curly::Presenter do
|
6
|
+
class CircusPresenter < Curly::Presenter
|
7
|
+
module MonkeyComponents
|
8
|
+
def monkey
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
include MonkeyComponents
|
13
|
+
|
14
|
+
presents :midget, :clown
|
15
|
+
attr_reader :midget, :clown
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets the presented parameters as instance variables" do
|
19
|
+
context = double("context")
|
20
|
+
|
21
|
+
presenter = CircusPresenter.new(context,
|
22
|
+
midget: "Meek Harolson",
|
23
|
+
clown: "Bubbles"
|
24
|
+
)
|
25
|
+
|
26
|
+
presenter.midget.should == "Meek Harolson"
|
27
|
+
presenter.clown.should == "Bubbles"
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
File without changes
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_support/core_ext/string/output_safety'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'curly/template_handler'
|
5
|
+
|
6
|
+
describe Curly::TemplateHandler do
|
7
|
+
let :presenter_class do
|
8
|
+
Class.new do
|
9
|
+
def initialize(context, options = {})
|
10
|
+
@context = context
|
11
|
+
@cache_key = options.fetch(:cache_key, nil)
|
12
|
+
@cache_duration = options.fetch(:cache_duration, nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def foo
|
16
|
+
"FOO"
|
17
|
+
end
|
18
|
+
|
19
|
+
def bar
|
20
|
+
@context.bar
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_key
|
24
|
+
@cache_key
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_duration
|
28
|
+
@cache_duration
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_available?(method)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
let :context_class do
|
38
|
+
Class.new do
|
39
|
+
attr_reader :output_buffer
|
40
|
+
attr_reader :local_assigns, :assigns
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@cache = Hash.new
|
44
|
+
@local_assigns = Hash.new
|
45
|
+
@assigns = Hash.new
|
46
|
+
@clock = 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def advance_clock(duration)
|
50
|
+
@clock += duration
|
51
|
+
end
|
52
|
+
|
53
|
+
def cache(key, options = {})
|
54
|
+
fragment, expired_at = @cache[key]
|
55
|
+
|
56
|
+
if fragment.nil? || @clock >= expired_at
|
57
|
+
old_buffer = @output_buffer
|
58
|
+
@output_buffer = ActiveSupport::SafeBuffer.new
|
59
|
+
|
60
|
+
yield
|
61
|
+
|
62
|
+
fragment = @output_buffer.to_s
|
63
|
+
duration = options[:expires_in] || Float::INFINITY
|
64
|
+
|
65
|
+
@cache[key] = [fragment, @clock + duration]
|
66
|
+
|
67
|
+
@output_buffer = old_buffer
|
68
|
+
end
|
69
|
+
|
70
|
+
safe_concat(fragment)
|
71
|
+
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def safe_concat(str)
|
76
|
+
@output_buffer.safe_concat(str)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
let(:template) { double("template", virtual_path: "test") }
|
82
|
+
let(:context) { context_class.new }
|
83
|
+
|
84
|
+
before do
|
85
|
+
stub_const("TestPresenter", presenter_class)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "passes in the presenter context to the presenter class" do
|
89
|
+
context.stub(:bar) { "BAR" }
|
90
|
+
template.stub(:source) { "{{bar}}" }
|
91
|
+
output.should == "BAR"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "allows calling public methods on the presenter" do
|
95
|
+
template.stub(:source) { "{{foo}}" }
|
96
|
+
output.should == "FOO"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "marks its output as HTML safe" do
|
100
|
+
template.stub(:source) { "{{foo}}" }
|
101
|
+
output.should be_html_safe
|
102
|
+
end
|
103
|
+
|
104
|
+
context "caching" do
|
105
|
+
before do
|
106
|
+
template.stub(:source) { "{{bar}}" }
|
107
|
+
context.stub(:bar) { "BAR" }
|
108
|
+
end
|
109
|
+
|
110
|
+
it "caches the result with the #cache_key from the presenter" do
|
111
|
+
context.assigns[:cache_key] = "x"
|
112
|
+
output.should == "BAR"
|
113
|
+
|
114
|
+
context.stub(:bar) { "BAZ" }
|
115
|
+
output.should == "BAR"
|
116
|
+
|
117
|
+
context.assigns[:cache_key] = "y"
|
118
|
+
output.should == "BAZ"
|
119
|
+
end
|
120
|
+
|
121
|
+
it "doesn't cache when the cache key is nil" do
|
122
|
+
context.assigns[:cache_key] = nil
|
123
|
+
output.should == "BAR"
|
124
|
+
|
125
|
+
context.stub(:bar) { "BAZ" }
|
126
|
+
output.should == "BAZ"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "adds a digest of the template source to the cache key" do
|
130
|
+
context.assigns[:cache_key] = "x"
|
131
|
+
|
132
|
+
template.stub(:source) { "{{bar}}" }
|
133
|
+
output.should == "BAR"
|
134
|
+
|
135
|
+
template.stub(:source) { "FOO{{bar}}" }
|
136
|
+
output.should == "FOOBAR"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "expires the cache keys after #cache_duration" do
|
140
|
+
context.assigns[:cache_key] = "x"
|
141
|
+
context.assigns[:cache_duration] = 42
|
142
|
+
|
143
|
+
output.should == "BAR"
|
144
|
+
|
145
|
+
context.stub(:bar) { "FOO" }
|
146
|
+
|
147
|
+
# Cached fragment has not yet expired.
|
148
|
+
context.advance_clock(41)
|
149
|
+
output.should == "BAR"
|
150
|
+
|
151
|
+
# Now it has! Huzzah!
|
152
|
+
context.advance_clock(1)
|
153
|
+
output.should == "FOO"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def output
|
158
|
+
code = Curly::TemplateHandler.call(template)
|
159
|
+
context.instance_eval(code)
|
160
|
+
end
|
161
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: curly-templates
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Daniel Schierbeck
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: actionpack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.11
|
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: 3.2.11
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
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: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.12.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.12.0
|
62
|
+
description: A view layer for your Rails apps that separates structure and logic.
|
63
|
+
email: dasch@zendesk.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- Gemfile
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- curly-templates.gemspec
|
72
|
+
- lib/curly.rb
|
73
|
+
- lib/curly/presenter.rb
|
74
|
+
- lib/curly/railtie.rb
|
75
|
+
- lib/curly/template_handler.rb
|
76
|
+
- spec/curly_spec.rb
|
77
|
+
- spec/presenter_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/template_handler_spec.rb
|
80
|
+
homepage: http://github.com/zendesk/curly-templates
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
hash: 924326065759708152
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 1.8.24
|
105
|
+
signing_key:
|
106
|
+
specification_version: 2
|
107
|
+
summary: Free your views!
|
108
|
+
test_files:
|
109
|
+
- spec/curly_spec.rb
|
110
|
+
- spec/presenter_spec.rb
|
111
|
+
- spec/template_handler_spec.rb
|