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 ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
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,8 @@
1
+ module Curly
2
+ class Railtie < Rails::Railtie
3
+ initializer 'curly.initialize_template_handler' do
4
+ require 'curly/template_handler'
5
+ ActionView::Template.register_template_handler :curly, Curly::TemplateHandler
6
+ end
7
+ end
8
+ 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
@@ -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 == "&lt;p&gt;dirty&lt;/p&gt;"
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
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