curly-templates 0.1.0
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/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
|