"
66 | end
67 |
68 | it "treats all values returned from the presenter as strings" do
69 | define_presenter do
70 | def foo; 42; end
71 | end
72 |
73 | expect(render("{{foo}}")).to eq "42"
74 | end
75 |
76 | it "removes comments from the output" do
77 | expect(render("hello{{! I'm a comment, yo }}world")).to eq "helloworld"
78 | end
79 |
80 | it "removes text in false blocks" do
81 | define_presenter do
82 | def false?
83 | false
84 | end
85 | end
86 |
87 | expect(render("{{#false?}}wut{{/false?}}")).to eq ""
88 | end
89 |
90 | it "keeps text in true blocks" do
91 | define_presenter do
92 | def true?
93 | true
94 | end
95 | end
96 |
97 | expect(render("{{#true?}}yello{{/true?}}")).to eq "yello"
98 | end
99 |
100 | it "removes text in inverse true blocks" do
101 | define_presenter do
102 | def true?
103 | true
104 | end
105 | end
106 |
107 | expect(render("{{^true?}}bar{{/true?}}")).to eq ""
108 | end
109 |
110 | it "keeps text in inverse false blocks" do
111 | define_presenter do
112 | def false?
113 | false
114 | end
115 | end
116 |
117 | expect(render("{{^false?}}yeah!{{/false?}}")).to eq "yeah!"
118 | end
119 |
120 | it "passes an argument to blocks" do
121 | define_presenter do
122 | def hello?(value)
123 | value == "world"
124 | end
125 | end
126 |
127 | expect(render("{{#hello.world?}}foo{{/hello.world?}}")).to eq "foo"
128 | expect(render("{{#hello.mars?}}bar{{/hello.mars?}}")).to eq ""
129 | end
130 |
131 | it "passes attributes to blocks" do
132 | define_presenter do
133 | def square?(width:, height:)
134 | width.to_i == height.to_i
135 | end
136 | end
137 |
138 | expect(render("{{#square? width=2 height=2}}yeah!{{/square?}}")).to eq "yeah!"
139 | end
140 |
141 | it "gives an error on incomplete blocks" do
142 | expect {
143 | render("{{#hello?}}")
144 | }.to raise_exception(Curly::IncompleteBlockError)
145 | end
146 |
147 | it "gives an error when closing unopened blocks" do
148 | expect {
149 | render("{{/goodbye?}}")
150 | }.to raise_exception(Curly::IncorrectEndingError)
151 | end
152 |
153 | it "gives an error on mismatching block ends" do
154 | expect {
155 | render("{{#x?}}{{#y?}}{{/x?}}{{/y?}}")
156 | }.to raise_exception(Curly::IncorrectEndingError)
157 | end
158 |
159 | it "does not execute arbitrary Ruby code" do
160 | expect(render('#{foo}')).to eq '#{foo}'
161 | end
162 | end
163 |
164 | describe ".valid?" do
165 | it "returns true if only available methods are referenced" do
166 | define_presenter do
167 | def foo; end
168 | end
169 |
170 | expect(validate("Hello, {{foo}}!")).to eq true
171 | end
172 |
173 | it "returns false if a missing method is referenced" do
174 | define_presenter
175 | expect(validate("Hello, {{i_am_missing}}")).to eq false
176 | end
177 |
178 | it "returns false if an unavailable method is referenced" do
179 | define_presenter do
180 | def self.available_components
181 | []
182 | end
183 | end
184 |
185 | expect(validate("Hello, {{inspect}}")).to eq false
186 | end
187 |
188 | def validate(template)
189 | Curly.valid?(template, ShowPresenter)
190 | end
191 | end
192 | end
193 |
--------------------------------------------------------------------------------
/lib/curly/compiler.rb:
--------------------------------------------------------------------------------
1 | require 'curly/scanner'
2 | require 'curly/parser'
3 | require 'curly/component_compiler'
4 | require 'curly/error'
5 | require 'curly/invalid_component'
6 |
7 | module Curly
8 |
9 | # Compiles Curly templates into executable Ruby code.
10 | #
11 | # A template must be accompanied by a presenter class. This class defines the
12 | # components that are valid within the template.
13 | #
14 | class Compiler
15 | # Compiles a Curly template to Ruby code.
16 | #
17 | # template - The template String that should be compiled.
18 | # presenter_class - The presenter Class.
19 | #
20 | # Raises InvalidComponent if the template contains a component that is not
21 | # allowed.
22 | # Raises IncorrectEndingError if a conditional block is not ended in the
23 | # correct order - the most recent block must be ended first.
24 | # Raises IncompleteBlockError if a block is not completed.
25 | # Returns a String containing the Ruby code.
26 | def self.compile(template, presenter_class)
27 | if presenter_class.nil?
28 | raise ArgumentError, "presenter class cannot be nil"
29 | end
30 |
31 | tokens = Scanner.scan(template)
32 | nodes = Parser.parse(tokens)
33 |
34 | compiler = new(presenter_class)
35 | compiler.compile(nodes)
36 | compiler.code
37 | end
38 |
39 | # Whether the Curly template is valid. This includes whether all
40 | # components are available on the presenter class.
41 | #
42 | # template - The template String that should be validated.
43 | # presenter_class - The presenter Class.
44 | #
45 | # Returns true if the template is valid, false otherwise.
46 | def self.valid?(template, presenter_class)
47 | compile(template, presenter_class)
48 |
49 | true
50 | rescue Error
51 | false
52 | end
53 |
54 | def initialize(presenter_class)
55 | @presenter_classes = [presenter_class]
56 | @parts = []
57 | end
58 |
59 | def compile(nodes)
60 | nodes.each do |node|
61 | send("compile_#{node.type}", node)
62 | end
63 | end
64 |
65 | def code
66 | <<-RUBY
67 | buffer = ActiveSupport::SafeBuffer.new
68 | buffers = []
69 | presenters = []
70 | options_stack = []
71 | #{@parts.join("\n")}
72 | buffer
73 | RUBY
74 | end
75 |
76 | private
77 |
78 | def presenter_class
79 | @presenter_classes.last
80 | end
81 |
82 | def compile_conditional(block)
83 | compile_conditional_block("if", block)
84 | end
85 |
86 | def compile_inverse_conditional(block)
87 | compile_conditional_block("unless", block)
88 | end
89 |
90 | def compile_collection(block)
91 | component = block.component
92 | method_call = ComponentCompiler.compile(presenter_class, component)
93 |
94 | name = component.name.singularize
95 | counter = "#{name}_counter"
96 |
97 | item_presenter_class = presenter_class.presenter_for_name(name)
98 |
99 | output <<-RUBY
100 | presenters << presenter
101 | options_stack << options
102 | items = Array(#{method_call})
103 | items.each_with_index do |item, index|
104 | options = options.merge("#{name}" => item, "#{counter}" => index + 1)
105 | presenter = ::#{item_presenter_class}.new(self, options)
106 | RUBY
107 |
108 | @presenter_classes.push(item_presenter_class)
109 | compile(block.nodes)
110 | @presenter_classes.pop
111 |
112 | output <<-RUBY
113 | end
114 | options = options_stack.pop
115 | presenter = presenters.pop
116 | RUBY
117 | end
118 |
119 | def compile_conditional_block(keyword, block)
120 | component = block.component
121 | method_call = ComponentCompiler.compile(presenter_class, component)
122 |
123 | unless component.name.end_with?("?")
124 | raise Curly::Error, "conditional components must end with `?`"
125 | end
126 |
127 | output <<-RUBY
128 | #{keyword} #{method_call}
129 | RUBY
130 |
131 | compile(block.nodes)
132 |
133 | output <<-RUBY
134 | end
135 | RUBY
136 | end
137 |
138 | def compile_context(block)
139 | component = block.component
140 | method_call = ComponentCompiler.compile(presenter_class, component, type: block.type)
141 |
142 | name = component.name
143 |
144 | item_presenter_class = presenter_class.presenter_for_name(name)
145 |
146 | output <<-RUBY
147 | options_stack << options
148 | presenters << presenter
149 | buffers << buffer
150 | buffer << #{method_call} do |item|
151 | options = options.merge("#{name}" => item)
152 | buffer = ActiveSupport::SafeBuffer.new
153 | presenter = ::#{item_presenter_class}.new(self, options)
154 | RUBY
155 |
156 | @presenter_classes.push(item_presenter_class)
157 | compile(block.nodes)
158 | @presenter_classes.pop
159 |
160 | output <<-RUBY
161 | buffer
162 | end
163 | buffer = buffers.pop
164 | presenter = presenters.pop
165 | options = options_stack.pop
166 | RUBY
167 | end
168 |
169 | def compile_component(component)
170 | method_call = ComponentCompiler.compile(presenter_class, component)
171 | code = "#{method_call} {|*args| yield(*args) }"
172 |
173 | output "buffer.concat(#{code.strip}.to_s)"
174 | end
175 |
176 | def compile_text(text)
177 | output "buffer.safe_concat(#{text.value.inspect})"
178 | end
179 |
180 | def compile_comment(comment)
181 | # Do nothing.
182 | end
183 |
184 | def output(code)
185 | @parts << code
186 | end
187 | end
188 | end
189 |
--------------------------------------------------------------------------------
/gemfiles/rails7.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/zendesk/genspec.git
3 | revision: 76116991caf40ef940076f702f70a141ced84ce2
4 | branch: rails-7
5 | specs:
6 | genspec (0.3.2)
7 | rspec (>= 2, < 4)
8 | thor
9 |
10 | PATH
11 | remote: ..
12 | specs:
13 | curly-templates (3.4.0)
14 | actionpack (>= 6.1)
15 | sorted_set
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (7.0.8)
21 | actionpack (= 7.0.8)
22 | activesupport (= 7.0.8)
23 | nio4r (~> 2.0)
24 | websocket-driver (>= 0.6.1)
25 | actionmailbox (7.0.8)
26 | actionpack (= 7.0.8)
27 | activejob (= 7.0.8)
28 | activerecord (= 7.0.8)
29 | activestorage (= 7.0.8)
30 | activesupport (= 7.0.8)
31 | mail (>= 2.7.1)
32 | net-imap
33 | net-pop
34 | net-smtp
35 | actionmailer (7.0.8)
36 | actionpack (= 7.0.8)
37 | actionview (= 7.0.8)
38 | activejob (= 7.0.8)
39 | activesupport (= 7.0.8)
40 | mail (~> 2.5, >= 2.5.4)
41 | net-imap
42 | net-pop
43 | net-smtp
44 | rails-dom-testing (~> 2.0)
45 | actionpack (7.0.8)
46 | actionview (= 7.0.8)
47 | activesupport (= 7.0.8)
48 | rack (~> 2.0, >= 2.2.4)
49 | rack-test (>= 0.6.3)
50 | rails-dom-testing (~> 2.0)
51 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
52 | actiontext (7.0.8)
53 | actionpack (= 7.0.8)
54 | activerecord (= 7.0.8)
55 | activestorage (= 7.0.8)
56 | activesupport (= 7.0.8)
57 | globalid (>= 0.6.0)
58 | nokogiri (>= 1.8.5)
59 | actionview (7.0.8)
60 | activesupport (= 7.0.8)
61 | builder (~> 3.1)
62 | erubi (~> 1.4)
63 | rails-dom-testing (~> 2.0)
64 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
65 | activejob (7.0.8)
66 | activesupport (= 7.0.8)
67 | globalid (>= 0.3.6)
68 | activemodel (7.0.8)
69 | activesupport (= 7.0.8)
70 | activerecord (7.0.8)
71 | activemodel (= 7.0.8)
72 | activesupport (= 7.0.8)
73 | activestorage (7.0.8)
74 | actionpack (= 7.0.8)
75 | activejob (= 7.0.8)
76 | activerecord (= 7.0.8)
77 | activesupport (= 7.0.8)
78 | marcel (~> 1.0)
79 | mini_mime (>= 1.1.0)
80 | activesupport (7.0.8)
81 | concurrent-ruby (~> 1.0, >= 1.0.2)
82 | i18n (>= 1.6, < 2)
83 | minitest (>= 5.1)
84 | tzinfo (~> 2.0)
85 | benchmark-ips (2.12.0)
86 | builder (3.2.4)
87 | concurrent-ruby (1.2.2)
88 | crass (1.0.6)
89 | date (3.3.4)
90 | diff-lcs (1.5.0)
91 | erubi (1.12.0)
92 | github-markup (4.0.2)
93 | globalid (1.2.1)
94 | activesupport (>= 6.1)
95 | i18n (1.14.1)
96 | concurrent-ruby (~> 1.0)
97 | loofah (2.21.4)
98 | crass (~> 1.0.2)
99 | nokogiri (>= 1.12.0)
100 | mail (2.8.1)
101 | mini_mime (>= 0.1.1)
102 | net-imap
103 | net-pop
104 | net-smtp
105 | marcel (1.0.2)
106 | method_source (1.0.0)
107 | mini_mime (1.1.5)
108 | mini_portile2 (2.8.5)
109 | minitest (5.20.0)
110 | net-imap (0.4.5)
111 | date
112 | net-protocol
113 | net-pop (0.1.2)
114 | net-protocol
115 | net-protocol (0.2.2)
116 | timeout
117 | net-smtp (0.4.0)
118 | net-protocol
119 | nio4r (2.5.9)
120 | nokogiri (1.15.4)
121 | mini_portile2 (~> 2.8.2)
122 | racc (~> 1.4)
123 | racc (1.7.3)
124 | rack (2.2.8)
125 | rack-test (2.1.0)
126 | rack (>= 1.3)
127 | rails (7.0.8)
128 | actioncable (= 7.0.8)
129 | actionmailbox (= 7.0.8)
130 | actionmailer (= 7.0.8)
131 | actionpack (= 7.0.8)
132 | actiontext (= 7.0.8)
133 | actionview (= 7.0.8)
134 | activejob (= 7.0.8)
135 | activemodel (= 7.0.8)
136 | activerecord (= 7.0.8)
137 | activestorage (= 7.0.8)
138 | activesupport (= 7.0.8)
139 | bundler (>= 1.15.0)
140 | railties (= 7.0.8)
141 | rails-dom-testing (2.2.0)
142 | activesupport (>= 5.0.0)
143 | minitest
144 | nokogiri (>= 1.6)
145 | rails-html-sanitizer (1.6.0)
146 | loofah (~> 2.21)
147 | nokogiri (~> 1.14)
148 | railties (7.0.8)
149 | actionpack (= 7.0.8)
150 | activesupport (= 7.0.8)
151 | method_source
152 | rake (>= 12.2)
153 | thor (~> 1.0)
154 | zeitwerk (~> 2.5)
155 | rake (13.1.0)
156 | rbtree (0.4.6)
157 | redcarpet (3.6.0)
158 | rspec (3.12.0)
159 | rspec-core (~> 3.12.0)
160 | rspec-expectations (~> 3.12.0)
161 | rspec-mocks (~> 3.12.0)
162 | rspec-core (3.12.2)
163 | rspec-support (~> 3.12.0)
164 | rspec-expectations (3.12.3)
165 | diff-lcs (>= 1.2.0, < 2.0)
166 | rspec-support (~> 3.12.0)
167 | rspec-mocks (3.12.6)
168 | diff-lcs (>= 1.2.0, < 2.0)
169 | rspec-support (~> 3.12.0)
170 | rspec-rails (6.0.3)
171 | actionpack (>= 6.1)
172 | activesupport (>= 6.1)
173 | railties (>= 6.1)
174 | rspec-core (~> 3.12)
175 | rspec-expectations (~> 3.12)
176 | rspec-mocks (~> 3.12)
177 | rspec-support (~> 3.12)
178 | rspec-support (3.12.1)
179 | set (1.0.3)
180 | sorted_set (1.0.3)
181 | rbtree
182 | set (~> 1.0)
183 | stackprof (0.2.25)
184 | thor (1.3.0)
185 | timeout (0.4.1)
186 | tomparse (0.4.2)
187 | tzinfo (2.0.6)
188 | concurrent-ruby (~> 1.0)
189 | websocket-driver (0.7.6)
190 | websocket-extensions (>= 0.1.0)
191 | websocket-extensions (0.1.5)
192 | yard (0.9.34)
193 | yard-tomdoc (0.7.1)
194 | tomparse (>= 0.4.0)
195 | yard
196 | zeitwerk (2.6.12)
197 |
198 | PLATFORMS
199 | ruby
200 |
201 | DEPENDENCIES
202 | benchmark-ips
203 | curly-templates!
204 | genspec!
205 | github-markup
206 | rails (~> 7.0.0)
207 | rake
208 | redcarpet
209 | rspec (>= 3)
210 | rspec-rails
211 | stackprof
212 | yard
213 | yard-tomdoc
214 |
215 | BUNDLED WITH
216 | 2.4.17
217 |
--------------------------------------------------------------------------------
/spec/template_handler_spec.rb:
--------------------------------------------------------------------------------
1 | describe Curly::TemplateHandler do
2 | let :presenter_class do
3 | Class.new do
4 | def initialize(context, options = {})
5 | @context = context
6 | @cache_key = options.fetch(:cache_key, nil)
7 | @cache_duration = options.fetch(:cache_duration, nil)
8 | @cache_options = options.fetch(:cache_options, {})
9 | end
10 |
11 | def setup!
12 | @context.content_for(:foo, "bar")
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 cache_options
32 | @cache_options
33 | end
34 |
35 | def self.component_available?(method)
36 | true
37 | end
38 | end
39 | end
40 |
41 | let :context_class do
42 | Class.new do
43 | attr_reader :output_buffer
44 | attr_reader :local_assigns, :assigns
45 |
46 | def initialize
47 | @cache = Hash.new
48 | @local_assigns = Hash.new
49 | @assigns = Hash.new
50 | @clock = 0
51 | end
52 |
53 | def reset!
54 | @output_buffer = ActiveSupport::SafeBuffer.new
55 | end
56 |
57 | def advance_clock(duration)
58 | @clock += duration
59 | end
60 |
61 | def content_for(key, value = nil)
62 | @contents ||= {}
63 | @contents[key] = value if value.present?
64 | @contents[key]
65 | end
66 |
67 | def cache(key, options = {})
68 | fragment, expired_at = @cache[key]
69 |
70 | if fragment.nil? || @clock >= expired_at
71 | old_buffer = @output_buffer
72 | @output_buffer = ActiveSupport::SafeBuffer.new
73 |
74 | yield
75 |
76 | fragment = @output_buffer.to_s
77 | duration = options[:expires_in] || Float::INFINITY
78 |
79 | @cache[key] = [fragment, @clock + duration]
80 |
81 | @output_buffer = old_buffer
82 | end
83 |
84 | safe_concat(fragment)
85 |
86 | nil
87 | end
88 |
89 | def safe_concat(str)
90 | @output_buffer.safe_concat(str)
91 | end
92 | end
93 | end
94 |
95 | let(:template) { double("template", virtual_path: "test") }
96 | let(:context) { context_class.new }
97 |
98 | before do
99 | stub_const("TestPresenter", presenter_class)
100 | end
101 |
102 | it "passes in the presenter context to the presenter class" do
103 | allow(context).to receive(:bar) { "BAR" }
104 | output = render("{{bar}}")
105 | expect(output).to eq "BAR"
106 | end
107 |
108 | it "should fail if there's no matching presenter class" do
109 | allow(template).to receive(:virtual_path) { "missing" }
110 | expect { render(" FOO ") }.to raise_exception(Curly::PresenterNotFound)
111 | end
112 |
113 | it "allows calling public methods on the presenter" do
114 | output = render("{{foo}}")
115 | expect(output).to eq "FOO"
116 | end
117 |
118 | it "marks its output as HTML safe" do
119 | output = render("{{foo}}")
120 | expect(output).to be_html_safe
121 | end
122 |
123 | it "calls the #setup! method before rendering the view" do
124 | output = render("{{foo}}")
125 | output
126 | expect(context.content_for(:foo)).to eq "bar"
127 | end
128 |
129 | context "caching" do
130 | before do
131 | allow(context).to receive(:bar) { "BAR" }
132 | end
133 |
134 | let(:output) { -> { render("{{bar}}") } }
135 |
136 | it "caches the result with the #cache_key from the presenter" do
137 | context.assigns[:cache_key] = "x"
138 | expect(output.call).to eq "BAR"
139 |
140 | allow(context).to receive(:bar) { "BAZ" }
141 | expect(output.call).to eq "BAR"
142 |
143 | context.assigns[:cache_key] = "y"
144 | expect(output.call).to eq "BAZ"
145 | end
146 |
147 | it "doesn't cache when the cache key is nil" do
148 | context.assigns[:cache_key] = nil
149 | expect(output.call).to eq "BAR"
150 |
151 | allow(context).to receive(:bar) { "BAZ" }
152 | expect(output.call).to eq "BAZ"
153 | end
154 |
155 | it "adds the presenter class' cache key to the instance's cache key" do
156 | # Make sure caching is enabled
157 | context.assigns[:cache_key] = "x"
158 |
159 | allow(presenter_class).to receive(:cache_key) { "foo" }
160 |
161 | expect(output.call).to eq "BAR"
162 |
163 | allow(presenter_class).to receive(:cache_key) { "bar" }
164 |
165 | allow(context).to receive(:bar) { "FOOBAR" }
166 | expect(output.call).to eq "FOOBAR"
167 | end
168 |
169 | it "expires the cache keys after #cache_duration" do
170 | context.assigns[:cache_key] = "x"
171 | context.assigns[:cache_duration] = 42
172 |
173 | expect(output.call).to eq "BAR"
174 |
175 | allow(context).to receive(:bar) { "FOO" }
176 |
177 | # Cached fragment has not yet expired.
178 | context.advance_clock(41)
179 | expect(output.call).to eq "BAR"
180 |
181 | # Now it has! Huzzah!
182 | context.advance_clock(1)
183 | expect(output.call).to eq "FOO"
184 | end
185 |
186 | it "passes #cache_options to the cache backend" do
187 | context.assigns[:cache_key] = "x"
188 | context.assigns[:cache_options] = { expires_in: 42 }
189 |
190 | expect(output.call).to eq "BAR"
191 |
192 | allow(context).to receive(:bar) { "FOO" }
193 |
194 | # Cached fragment has not yet expired.
195 | context.advance_clock(41)
196 | expect(output.call).to eq "BAR"
197 |
198 | # Now it has! Huzzah!
199 | context.advance_clock(1)
200 | expect(output.call).to eq "FOO"
201 | end
202 | end
203 |
204 | def render(source)
205 | code = Curly::TemplateHandler.call(template, source)
206 |
207 | context.reset!
208 | context.instance_eval(code)
209 | end
210 | end
211 |
--------------------------------------------------------------------------------
/gemfiles/rails6.1.gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/zendesk/genspec.git
3 | revision: 76116991caf40ef940076f702f70a141ced84ce2
4 | branch: rails-7
5 | specs:
6 | genspec (0.3.2)
7 | rspec (>= 2, < 4)
8 | thor
9 |
10 | PATH
11 | remote: ..
12 | specs:
13 | curly-templates (3.4.0)
14 | actionpack (>= 6.1)
15 | sorted_set
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (6.1.7.6)
21 | actionpack (= 6.1.7.6)
22 | activesupport (= 6.1.7.6)
23 | nio4r (~> 2.0)
24 | websocket-driver (>= 0.6.1)
25 | actionmailbox (6.1.7.6)
26 | actionpack (= 6.1.7.6)
27 | activejob (= 6.1.7.6)
28 | activerecord (= 6.1.7.6)
29 | activestorage (= 6.1.7.6)
30 | activesupport (= 6.1.7.6)
31 | mail (>= 2.7.1)
32 | actionmailer (6.1.7.6)
33 | actionpack (= 6.1.7.6)
34 | actionview (= 6.1.7.6)
35 | activejob (= 6.1.7.6)
36 | activesupport (= 6.1.7.6)
37 | mail (~> 2.5, >= 2.5.4)
38 | rails-dom-testing (~> 2.0)
39 | actionpack (6.1.7.6)
40 | actionview (= 6.1.7.6)
41 | activesupport (= 6.1.7.6)
42 | rack (~> 2.0, >= 2.0.9)
43 | rack-test (>= 0.6.3)
44 | rails-dom-testing (~> 2.0)
45 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
46 | actiontext (6.1.7.6)
47 | actionpack (= 6.1.7.6)
48 | activerecord (= 6.1.7.6)
49 | activestorage (= 6.1.7.6)
50 | activesupport (= 6.1.7.6)
51 | nokogiri (>= 1.8.5)
52 | actionview (6.1.7.6)
53 | activesupport (= 6.1.7.6)
54 | builder (~> 3.1)
55 | erubi (~> 1.4)
56 | rails-dom-testing (~> 2.0)
57 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
58 | activejob (6.1.7.6)
59 | activesupport (= 6.1.7.6)
60 | globalid (>= 0.3.6)
61 | activemodel (6.1.7.6)
62 | activesupport (= 6.1.7.6)
63 | activerecord (6.1.7.6)
64 | activemodel (= 6.1.7.6)
65 | activesupport (= 6.1.7.6)
66 | activestorage (6.1.7.6)
67 | actionpack (= 6.1.7.6)
68 | activejob (= 6.1.7.6)
69 | activerecord (= 6.1.7.6)
70 | activesupport (= 6.1.7.6)
71 | marcel (~> 1.0)
72 | mini_mime (>= 1.1.0)
73 | activesupport (6.1.7.6)
74 | concurrent-ruby (~> 1.0, >= 1.0.2)
75 | i18n (>= 1.6, < 2)
76 | minitest (>= 5.1)
77 | tzinfo (~> 2.0)
78 | zeitwerk (~> 2.3)
79 | benchmark-ips (2.12.0)
80 | builder (3.2.4)
81 | concurrent-ruby (1.2.2)
82 | crass (1.0.6)
83 | date (3.3.4)
84 | diff-lcs (1.5.0)
85 | erubi (1.12.0)
86 | github-markup (4.0.2)
87 | globalid (1.2.1)
88 | activesupport (>= 6.1)
89 | i18n (1.14.1)
90 | concurrent-ruby (~> 1.0)
91 | loofah (2.21.4)
92 | crass (~> 1.0.2)
93 | nokogiri (>= 1.12.0)
94 | mail (2.8.1)
95 | mini_mime (>= 0.1.1)
96 | net-imap
97 | net-pop
98 | net-smtp
99 | marcel (1.0.2)
100 | method_source (1.0.0)
101 | mini_mime (1.1.5)
102 | mini_portile2 (2.8.5)
103 | minitest (5.20.0)
104 | net-imap (0.4.5)
105 | date
106 | net-protocol
107 | net-pop (0.1.2)
108 | net-protocol
109 | net-protocol (0.2.2)
110 | timeout
111 | net-smtp (0.4.0)
112 | net-protocol
113 | nio4r (2.5.9)
114 | nokogiri (1.15.4)
115 | mini_portile2 (~> 2.8.2)
116 | racc (~> 1.4)
117 | racc (1.7.3)
118 | rack (2.2.8)
119 | rack-test (2.1.0)
120 | rack (>= 1.3)
121 | rails (6.1.7.6)
122 | actioncable (= 6.1.7.6)
123 | actionmailbox (= 6.1.7.6)
124 | actionmailer (= 6.1.7.6)
125 | actionpack (= 6.1.7.6)
126 | actiontext (= 6.1.7.6)
127 | actionview (= 6.1.7.6)
128 | activejob (= 6.1.7.6)
129 | activemodel (= 6.1.7.6)
130 | activerecord (= 6.1.7.6)
131 | activestorage (= 6.1.7.6)
132 | activesupport (= 6.1.7.6)
133 | bundler (>= 1.15.0)
134 | railties (= 6.1.7.6)
135 | sprockets-rails (>= 2.0.0)
136 | rails-dom-testing (2.2.0)
137 | activesupport (>= 5.0.0)
138 | minitest
139 | nokogiri (>= 1.6)
140 | rails-html-sanitizer (1.6.0)
141 | loofah (~> 2.21)
142 | nokogiri (~> 1.14)
143 | railties (6.1.7.6)
144 | actionpack (= 6.1.7.6)
145 | activesupport (= 6.1.7.6)
146 | method_source
147 | rake (>= 12.2)
148 | thor (~> 1.0)
149 | rake (13.1.0)
150 | rbtree (0.4.6)
151 | redcarpet (3.6.0)
152 | rspec (3.12.0)
153 | rspec-core (~> 3.12.0)
154 | rspec-expectations (~> 3.12.0)
155 | rspec-mocks (~> 3.12.0)
156 | rspec-core (3.12.2)
157 | rspec-support (~> 3.12.0)
158 | rspec-expectations (3.12.3)
159 | diff-lcs (>= 1.2.0, < 2.0)
160 | rspec-support (~> 3.12.0)
161 | rspec-mocks (3.12.6)
162 | diff-lcs (>= 1.2.0, < 2.0)
163 | rspec-support (~> 3.12.0)
164 | rspec-rails (6.0.3)
165 | actionpack (>= 6.1)
166 | activesupport (>= 6.1)
167 | railties (>= 6.1)
168 | rspec-core (~> 3.12)
169 | rspec-expectations (~> 3.12)
170 | rspec-mocks (~> 3.12)
171 | rspec-support (~> 3.12)
172 | rspec-support (3.12.1)
173 | set (1.0.3)
174 | sorted_set (1.0.3)
175 | rbtree
176 | set (~> 1.0)
177 | sprockets (4.2.1)
178 | concurrent-ruby (~> 1.0)
179 | rack (>= 2.2.4, < 4)
180 | sprockets-rails (3.4.2)
181 | actionpack (>= 5.2)
182 | activesupport (>= 5.2)
183 | sprockets (>= 3.0.0)
184 | stackprof (0.2.25)
185 | thor (1.3.0)
186 | timeout (0.4.1)
187 | tomparse (0.4.2)
188 | tzinfo (2.0.6)
189 | concurrent-ruby (~> 1.0)
190 | websocket-driver (0.7.6)
191 | websocket-extensions (>= 0.1.0)
192 | websocket-extensions (0.1.5)
193 | yard (0.9.34)
194 | yard-tomdoc (0.7.1)
195 | tomparse (>= 0.4.0)
196 | yard
197 | zeitwerk (2.6.12)
198 |
199 | PLATFORMS
200 | ruby
201 |
202 | DEPENDENCIES
203 | benchmark-ips
204 | curly-templates!
205 | genspec!
206 | github-markup
207 | rails (~> 6.1.0)
208 | rake
209 | redcarpet
210 | rspec (>= 3)
211 | rspec-rails
212 | stackprof
213 | yard
214 | yard-tomdoc
215 |
216 | BUNDLED WITH
217 | 2.4.17
218 |
--------------------------------------------------------------------------------
/gemfiles/rails7.1.gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/zendesk/genspec.git
3 | revision: 76116991caf40ef940076f702f70a141ced84ce2
4 | branch: rails-7
5 | specs:
6 | genspec (0.3.2)
7 | rspec (>= 2, < 4)
8 | thor
9 |
10 | PATH
11 | remote: ..
12 | specs:
13 | curly-templates (3.4.0)
14 | actionpack (>= 6.1)
15 | sorted_set
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (7.1.2)
21 | actionpack (= 7.1.2)
22 | activesupport (= 7.1.2)
23 | nio4r (~> 2.0)
24 | websocket-driver (>= 0.6.1)
25 | zeitwerk (~> 2.6)
26 | actionmailbox (7.1.2)
27 | actionpack (= 7.1.2)
28 | activejob (= 7.1.2)
29 | activerecord (= 7.1.2)
30 | activestorage (= 7.1.2)
31 | activesupport (= 7.1.2)
32 | mail (>= 2.7.1)
33 | net-imap
34 | net-pop
35 | net-smtp
36 | actionmailer (7.1.2)
37 | actionpack (= 7.1.2)
38 | actionview (= 7.1.2)
39 | activejob (= 7.1.2)
40 | activesupport (= 7.1.2)
41 | mail (~> 2.5, >= 2.5.4)
42 | net-imap
43 | net-pop
44 | net-smtp
45 | rails-dom-testing (~> 2.2)
46 | actionpack (7.1.2)
47 | actionview (= 7.1.2)
48 | activesupport (= 7.1.2)
49 | nokogiri (>= 1.8.5)
50 | racc
51 | rack (>= 2.2.4)
52 | rack-session (>= 1.0.1)
53 | rack-test (>= 0.6.3)
54 | rails-dom-testing (~> 2.2)
55 | rails-html-sanitizer (~> 1.6)
56 | actiontext (7.1.2)
57 | actionpack (= 7.1.2)
58 | activerecord (= 7.1.2)
59 | activestorage (= 7.1.2)
60 | activesupport (= 7.1.2)
61 | globalid (>= 0.6.0)
62 | nokogiri (>= 1.8.5)
63 | actionview (7.1.2)
64 | activesupport (= 7.1.2)
65 | builder (~> 3.1)
66 | erubi (~> 1.11)
67 | rails-dom-testing (~> 2.2)
68 | rails-html-sanitizer (~> 1.6)
69 | activejob (7.1.2)
70 | activesupport (= 7.1.2)
71 | globalid (>= 0.3.6)
72 | activemodel (7.1.2)
73 | activesupport (= 7.1.2)
74 | activerecord (7.1.2)
75 | activemodel (= 7.1.2)
76 | activesupport (= 7.1.2)
77 | timeout (>= 0.4.0)
78 | activestorage (7.1.2)
79 | actionpack (= 7.1.2)
80 | activejob (= 7.1.2)
81 | activerecord (= 7.1.2)
82 | activesupport (= 7.1.2)
83 | marcel (~> 1.0)
84 | activesupport (7.1.2)
85 | base64
86 | bigdecimal
87 | concurrent-ruby (~> 1.0, >= 1.0.2)
88 | connection_pool (>= 2.2.5)
89 | drb
90 | i18n (>= 1.6, < 2)
91 | minitest (>= 5.1)
92 | mutex_m
93 | tzinfo (~> 2.0)
94 | base64 (0.2.0)
95 | benchmark-ips (2.12.0)
96 | bigdecimal (3.1.4)
97 | builder (3.2.4)
98 | concurrent-ruby (1.2.2)
99 | connection_pool (2.4.1)
100 | crass (1.0.6)
101 | date (3.3.4)
102 | diff-lcs (1.5.0)
103 | drb (2.2.1)
104 | erubi (1.12.0)
105 | github-markup (4.0.2)
106 | globalid (1.2.1)
107 | activesupport (>= 6.1)
108 | i18n (1.14.1)
109 | concurrent-ruby (~> 1.0)
110 | io-console (0.6.0)
111 | irb (1.9.0)
112 | rdoc
113 | reline (>= 0.3.8)
114 | loofah (2.21.4)
115 | crass (~> 1.0.2)
116 | nokogiri (>= 1.12.0)
117 | mail (2.8.1)
118 | mini_mime (>= 0.1.1)
119 | net-imap
120 | net-pop
121 | net-smtp
122 | marcel (1.0.2)
123 | mini_mime (1.1.5)
124 | mini_portile2 (2.8.5)
125 | minitest (5.20.0)
126 | mutex_m (0.2.0)
127 | net-imap (0.4.5)
128 | date
129 | net-protocol
130 | net-pop (0.1.2)
131 | net-protocol
132 | net-protocol (0.2.2)
133 | timeout
134 | net-smtp (0.4.0)
135 | net-protocol
136 | nio4r (2.5.9)
137 | nokogiri (1.15.4)
138 | mini_portile2 (~> 2.8.2)
139 | racc (~> 1.4)
140 | psych (5.1.1.1)
141 | stringio
142 | racc (1.7.3)
143 | rack (3.0.8)
144 | rack-session (2.0.0)
145 | rack (>= 3.0.0)
146 | rack-test (2.1.0)
147 | rack (>= 1.3)
148 | rackup (2.1.0)
149 | rack (>= 3)
150 | webrick (~> 1.8)
151 | rails (7.1.2)
152 | actioncable (= 7.1.2)
153 | actionmailbox (= 7.1.2)
154 | actionmailer (= 7.1.2)
155 | actionpack (= 7.1.2)
156 | actiontext (= 7.1.2)
157 | actionview (= 7.1.2)
158 | activejob (= 7.1.2)
159 | activemodel (= 7.1.2)
160 | activerecord (= 7.1.2)
161 | activestorage (= 7.1.2)
162 | activesupport (= 7.1.2)
163 | bundler (>= 1.15.0)
164 | railties (= 7.1.2)
165 | rails-dom-testing (2.2.0)
166 | activesupport (>= 5.0.0)
167 | minitest
168 | nokogiri (>= 1.6)
169 | rails-html-sanitizer (1.6.0)
170 | loofah (~> 2.21)
171 | nokogiri (~> 1.14)
172 | railties (7.1.2)
173 | actionpack (= 7.1.2)
174 | activesupport (= 7.1.2)
175 | irb
176 | rackup (>= 1.0.0)
177 | rake (>= 12.2)
178 | thor (~> 1.0, >= 1.2.2)
179 | zeitwerk (~> 2.6)
180 | rake (13.1.0)
181 | rbtree (0.4.6)
182 | rdoc (6.6.0)
183 | psych (>= 4.0.0)
184 | redcarpet (3.6.0)
185 | reline (0.4.0)
186 | io-console (~> 0.5)
187 | rspec (3.12.0)
188 | rspec-core (~> 3.12.0)
189 | rspec-expectations (~> 3.12.0)
190 | rspec-mocks (~> 3.12.0)
191 | rspec-core (3.12.2)
192 | rspec-support (~> 3.12.0)
193 | rspec-expectations (3.12.3)
194 | diff-lcs (>= 1.2.0, < 2.0)
195 | rspec-support (~> 3.12.0)
196 | rspec-mocks (3.12.6)
197 | diff-lcs (>= 1.2.0, < 2.0)
198 | rspec-support (~> 3.12.0)
199 | rspec-rails (6.0.3)
200 | actionpack (>= 6.1)
201 | activesupport (>= 6.1)
202 | railties (>= 6.1)
203 | rspec-core (~> 3.12)
204 | rspec-expectations (~> 3.12)
205 | rspec-mocks (~> 3.12)
206 | rspec-support (~> 3.12)
207 | rspec-support (3.12.1)
208 | set (1.0.3)
209 | sorted_set (1.0.3)
210 | rbtree
211 | set (~> 1.0)
212 | stackprof (0.2.25)
213 | stringio (3.0.9)
214 | thor (1.3.0)
215 | timeout (0.4.1)
216 | tomparse (0.4.2)
217 | tzinfo (2.0.6)
218 | concurrent-ruby (~> 1.0)
219 | webrick (1.8.1)
220 | websocket-driver (0.7.6)
221 | websocket-extensions (>= 0.1.0)
222 | websocket-extensions (0.1.5)
223 | yard (0.9.34)
224 | yard-tomdoc (0.7.1)
225 | tomparse (>= 0.4.0)
226 | yard
227 | zeitwerk (2.6.12)
228 |
229 | PLATFORMS
230 | ruby
231 |
232 | DEPENDENCIES
233 | benchmark-ips
234 | curly-templates!
235 | genspec!
236 | github-markup
237 | rails (~> 7.1.0)
238 | rake
239 | redcarpet
240 | rspec (>= 3)
241 | rspec-rails
242 | stackprof
243 | yard
244 | yard-tomdoc
245 |
246 | BUNDLED WITH
247 | 2.4.17
248 |
--------------------------------------------------------------------------------
/gemfiles/rails8.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/zendesk/genspec.git
3 | revision: 20caa3263a6780aaf40629b8137f0770c849bc49
4 | branch: rails-8
5 | specs:
6 | genspec (0.3.2)
7 | rspec (>= 2, < 4)
8 | thor
9 |
10 | PATH
11 | remote: ..
12 | specs:
13 | curly-templates (3.4.0)
14 | actionpack (>= 6.1)
15 | sorted_set
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (8.0.1)
21 | actionpack (= 8.0.1)
22 | activesupport (= 8.0.1)
23 | nio4r (~> 2.0)
24 | websocket-driver (>= 0.6.1)
25 | zeitwerk (~> 2.6)
26 | actionmailbox (8.0.1)
27 | actionpack (= 8.0.1)
28 | activejob (= 8.0.1)
29 | activerecord (= 8.0.1)
30 | activestorage (= 8.0.1)
31 | activesupport (= 8.0.1)
32 | mail (>= 2.8.0)
33 | actionmailer (8.0.1)
34 | actionpack (= 8.0.1)
35 | actionview (= 8.0.1)
36 | activejob (= 8.0.1)
37 | activesupport (= 8.0.1)
38 | mail (>= 2.8.0)
39 | rails-dom-testing (~> 2.2)
40 | actionpack (8.0.1)
41 | actionview (= 8.0.1)
42 | activesupport (= 8.0.1)
43 | nokogiri (>= 1.8.5)
44 | rack (>= 2.2.4)
45 | rack-session (>= 1.0.1)
46 | rack-test (>= 0.6.3)
47 | rails-dom-testing (~> 2.2)
48 | rails-html-sanitizer (~> 1.6)
49 | useragent (~> 0.16)
50 | actiontext (8.0.1)
51 | actionpack (= 8.0.1)
52 | activerecord (= 8.0.1)
53 | activestorage (= 8.0.1)
54 | activesupport (= 8.0.1)
55 | globalid (>= 0.6.0)
56 | nokogiri (>= 1.8.5)
57 | actionview (8.0.1)
58 | activesupport (= 8.0.1)
59 | builder (~> 3.1)
60 | erubi (~> 1.11)
61 | rails-dom-testing (~> 2.2)
62 | rails-html-sanitizer (~> 1.6)
63 | activejob (8.0.1)
64 | activesupport (= 8.0.1)
65 | globalid (>= 0.3.6)
66 | activemodel (8.0.1)
67 | activesupport (= 8.0.1)
68 | activerecord (8.0.1)
69 | activemodel (= 8.0.1)
70 | activesupport (= 8.0.1)
71 | timeout (>= 0.4.0)
72 | activestorage (8.0.1)
73 | actionpack (= 8.0.1)
74 | activejob (= 8.0.1)
75 | activerecord (= 8.0.1)
76 | activesupport (= 8.0.1)
77 | marcel (~> 1.0)
78 | activesupport (8.0.1)
79 | base64
80 | benchmark (>= 0.3)
81 | bigdecimal
82 | concurrent-ruby (~> 1.0, >= 1.3.1)
83 | connection_pool (>= 2.2.5)
84 | drb
85 | i18n (>= 1.6, < 2)
86 | logger (>= 1.4.2)
87 | minitest (>= 5.1)
88 | securerandom (>= 0.3)
89 | tzinfo (~> 2.0, >= 2.0.5)
90 | uri (>= 0.13.1)
91 | base64 (0.2.0)
92 | benchmark (0.4.0)
93 | benchmark-ips (2.14.0)
94 | bigdecimal (3.1.9)
95 | builder (3.3.0)
96 | concurrent-ruby (1.3.4)
97 | connection_pool (2.4.1)
98 | crass (1.0.6)
99 | date (3.4.1)
100 | diff-lcs (1.5.1)
101 | drb (2.2.1)
102 | erubi (1.13.1)
103 | github-markup (5.0.1)
104 | globalid (1.2.1)
105 | activesupport (>= 6.1)
106 | i18n (1.14.6)
107 | concurrent-ruby (~> 1.0)
108 | io-console (0.8.0)
109 | irb (1.14.3)
110 | rdoc (>= 4.0.0)
111 | reline (>= 0.4.2)
112 | logger (1.6.4)
113 | loofah (2.23.1)
114 | crass (~> 1.0.2)
115 | nokogiri (>= 1.12.0)
116 | mail (2.8.1)
117 | mini_mime (>= 0.1.1)
118 | net-imap
119 | net-pop
120 | net-smtp
121 | marcel (1.0.4)
122 | mini_mime (1.1.5)
123 | mini_portile2 (2.8.8)
124 | minitest (5.25.4)
125 | net-imap (0.5.4)
126 | date
127 | net-protocol
128 | net-pop (0.1.2)
129 | net-protocol
130 | net-protocol (0.2.2)
131 | timeout
132 | net-smtp (0.5.0)
133 | net-protocol
134 | nio4r (2.7.4)
135 | nokogiri (1.18.1)
136 | mini_portile2 (~> 2.8.2)
137 | racc (~> 1.4)
138 | psych (5.2.2)
139 | date
140 | stringio
141 | racc (1.8.1)
142 | rack (3.1.8)
143 | rack-session (2.0.0)
144 | rack (>= 3.0.0)
145 | rack-test (2.2.0)
146 | rack (>= 1.3)
147 | rackup (2.2.1)
148 | rack (>= 3)
149 | rails (8.0.1)
150 | actioncable (= 8.0.1)
151 | actionmailbox (= 8.0.1)
152 | actionmailer (= 8.0.1)
153 | actionpack (= 8.0.1)
154 | actiontext (= 8.0.1)
155 | actionview (= 8.0.1)
156 | activejob (= 8.0.1)
157 | activemodel (= 8.0.1)
158 | activerecord (= 8.0.1)
159 | activestorage (= 8.0.1)
160 | activesupport (= 8.0.1)
161 | bundler (>= 1.15.0)
162 | railties (= 8.0.1)
163 | rails-dom-testing (2.2.0)
164 | activesupport (>= 5.0.0)
165 | minitest
166 | nokogiri (>= 1.6)
167 | rails-html-sanitizer (1.6.2)
168 | loofah (~> 2.21)
169 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
170 | railties (8.0.1)
171 | actionpack (= 8.0.1)
172 | activesupport (= 8.0.1)
173 | irb (~> 1.13)
174 | rackup (>= 1.0.0)
175 | rake (>= 12.2)
176 | thor (~> 1.0, >= 1.2.2)
177 | zeitwerk (~> 2.6)
178 | rake (13.2.1)
179 | rbtree (0.4.6)
180 | rdoc (6.10.0)
181 | psych (>= 4.0.0)
182 | redcarpet (3.6.0)
183 | reline (0.6.0)
184 | io-console (~> 0.5)
185 | rspec (3.13.0)
186 | rspec-core (~> 3.13.0)
187 | rspec-expectations (~> 3.13.0)
188 | rspec-mocks (~> 3.13.0)
189 | rspec-core (3.13.2)
190 | rspec-support (~> 3.13.0)
191 | rspec-expectations (3.13.3)
192 | diff-lcs (>= 1.2.0, < 2.0)
193 | rspec-support (~> 3.13.0)
194 | rspec-mocks (3.13.2)
195 | diff-lcs (>= 1.2.0, < 2.0)
196 | rspec-support (~> 3.13.0)
197 | rspec-rails (7.1.0)
198 | actionpack (>= 7.0)
199 | activesupport (>= 7.0)
200 | railties (>= 7.0)
201 | rspec-core (~> 3.13)
202 | rspec-expectations (~> 3.13)
203 | rspec-mocks (~> 3.13)
204 | rspec-support (~> 3.13)
205 | rspec-support (3.13.2)
206 | securerandom (0.4.1)
207 | set (1.1.1)
208 | sorted_set (1.0.3)
209 | rbtree
210 | set (~> 1.0)
211 | stackprof (0.2.26)
212 | stringio (3.1.2)
213 | thor (1.3.2)
214 | timeout (0.4.3)
215 | tomparse (0.4.2)
216 | tzinfo (2.0.6)
217 | concurrent-ruby (~> 1.0)
218 | uri (1.0.2)
219 | useragent (0.16.11)
220 | websocket-driver (0.7.6)
221 | websocket-extensions (>= 0.1.0)
222 | websocket-extensions (0.1.5)
223 | yard (0.9.37)
224 | yard-tomdoc (0.7.1)
225 | tomparse (>= 0.4.0)
226 | yard
227 | zeitwerk (2.7.1)
228 |
229 | PLATFORMS
230 | ruby
231 |
232 | DEPENDENCIES
233 | benchmark-ips
234 | curly-templates!
235 | genspec!
236 | github-markup
237 | rails (~> 8.0.0)
238 | rake
239 | redcarpet
240 | rspec (>= 3)
241 | rspec-rails
242 | stackprof
243 | yard
244 | yard-tomdoc
245 |
246 | BUNDLED WITH
247 | 2.5.17
248 |
--------------------------------------------------------------------------------
/gemfiles/rails7.2.gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/zendesk/genspec.git
3 | revision: 76116991caf40ef940076f702f70a141ced84ce2
4 | branch: rails-7
5 | specs:
6 | genspec (0.3.2)
7 | rspec (>= 2, < 4)
8 | thor
9 |
10 | PATH
11 | remote: ..
12 | specs:
13 | curly-templates (3.4.0)
14 | actionpack (>= 6.1)
15 | sorted_set
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (7.2.2.1)
21 | actionpack (= 7.2.2.1)
22 | activesupport (= 7.2.2.1)
23 | nio4r (~> 2.0)
24 | websocket-driver (>= 0.6.1)
25 | zeitwerk (~> 2.6)
26 | actionmailbox (7.2.2.1)
27 | actionpack (= 7.2.2.1)
28 | activejob (= 7.2.2.1)
29 | activerecord (= 7.2.2.1)
30 | activestorage (= 7.2.2.1)
31 | activesupport (= 7.2.2.1)
32 | mail (>= 2.8.0)
33 | actionmailer (7.2.2.1)
34 | actionpack (= 7.2.2.1)
35 | actionview (= 7.2.2.1)
36 | activejob (= 7.2.2.1)
37 | activesupport (= 7.2.2.1)
38 | mail (>= 2.8.0)
39 | rails-dom-testing (~> 2.2)
40 | actionpack (7.2.2.1)
41 | actionview (= 7.2.2.1)
42 | activesupport (= 7.2.2.1)
43 | nokogiri (>= 1.8.5)
44 | racc
45 | rack (>= 2.2.4, < 3.2)
46 | rack-session (>= 1.0.1)
47 | rack-test (>= 0.6.3)
48 | rails-dom-testing (~> 2.2)
49 | rails-html-sanitizer (~> 1.6)
50 | useragent (~> 0.16)
51 | actiontext (7.2.2.1)
52 | actionpack (= 7.2.2.1)
53 | activerecord (= 7.2.2.1)
54 | activestorage (= 7.2.2.1)
55 | activesupport (= 7.2.2.1)
56 | globalid (>= 0.6.0)
57 | nokogiri (>= 1.8.5)
58 | actionview (7.2.2.1)
59 | activesupport (= 7.2.2.1)
60 | builder (~> 3.1)
61 | erubi (~> 1.11)
62 | rails-dom-testing (~> 2.2)
63 | rails-html-sanitizer (~> 1.6)
64 | activejob (7.2.2.1)
65 | activesupport (= 7.2.2.1)
66 | globalid (>= 0.3.6)
67 | activemodel (7.2.2.1)
68 | activesupport (= 7.2.2.1)
69 | activerecord (7.2.2.1)
70 | activemodel (= 7.2.2.1)
71 | activesupport (= 7.2.2.1)
72 | timeout (>= 0.4.0)
73 | activestorage (7.2.2.1)
74 | actionpack (= 7.2.2.1)
75 | activejob (= 7.2.2.1)
76 | activerecord (= 7.2.2.1)
77 | activesupport (= 7.2.2.1)
78 | marcel (~> 1.0)
79 | activesupport (7.2.2.1)
80 | base64
81 | benchmark (>= 0.3)
82 | bigdecimal
83 | concurrent-ruby (~> 1.0, >= 1.3.1)
84 | connection_pool (>= 2.2.5)
85 | drb
86 | i18n (>= 1.6, < 2)
87 | logger (>= 1.4.2)
88 | minitest (>= 5.1)
89 | securerandom (>= 0.3)
90 | tzinfo (~> 2.0, >= 2.0.5)
91 | base64 (0.2.0)
92 | benchmark (0.4.0)
93 | benchmark-ips (2.14.0)
94 | bigdecimal (3.1.9)
95 | builder (3.3.0)
96 | concurrent-ruby (1.3.4)
97 | connection_pool (2.4.1)
98 | crass (1.0.6)
99 | date (3.4.1)
100 | diff-lcs (1.5.1)
101 | drb (2.2.1)
102 | erubi (1.13.1)
103 | github-markup (5.0.1)
104 | globalid (1.2.1)
105 | activesupport (>= 6.1)
106 | i18n (1.14.6)
107 | concurrent-ruby (~> 1.0)
108 | io-console (0.8.0)
109 | irb (1.14.3)
110 | rdoc (>= 4.0.0)
111 | reline (>= 0.4.2)
112 | logger (1.6.4)
113 | loofah (2.23.1)
114 | crass (~> 1.0.2)
115 | nokogiri (>= 1.12.0)
116 | mail (2.8.1)
117 | mini_mime (>= 0.1.1)
118 | net-imap
119 | net-pop
120 | net-smtp
121 | marcel (1.0.4)
122 | mini_mime (1.1.5)
123 | mini_portile2 (2.8.8)
124 | minitest (5.25.4)
125 | net-imap (0.5.4)
126 | date
127 | net-protocol
128 | net-pop (0.1.2)
129 | net-protocol
130 | net-protocol (0.2.2)
131 | timeout
132 | net-smtp (0.5.0)
133 | net-protocol
134 | nio4r (2.7.4)
135 | nokogiri (1.18.1)
136 | mini_portile2 (~> 2.8.2)
137 | racc (~> 1.4)
138 | psych (5.2.2)
139 | date
140 | stringio
141 | racc (1.8.1)
142 | rack (3.1.8)
143 | rack-session (2.0.0)
144 | rack (>= 3.0.0)
145 | rack-test (2.2.0)
146 | rack (>= 1.3)
147 | rackup (2.2.1)
148 | rack (>= 3)
149 | rails (7.2.2.1)
150 | actioncable (= 7.2.2.1)
151 | actionmailbox (= 7.2.2.1)
152 | actionmailer (= 7.2.2.1)
153 | actionpack (= 7.2.2.1)
154 | actiontext (= 7.2.2.1)
155 | actionview (= 7.2.2.1)
156 | activejob (= 7.2.2.1)
157 | activemodel (= 7.2.2.1)
158 | activerecord (= 7.2.2.1)
159 | activestorage (= 7.2.2.1)
160 | activesupport (= 7.2.2.1)
161 | bundler (>= 1.15.0)
162 | railties (= 7.2.2.1)
163 | rails-dom-testing (2.2.0)
164 | activesupport (>= 5.0.0)
165 | minitest
166 | nokogiri (>= 1.6)
167 | rails-html-sanitizer (1.6.2)
168 | loofah (~> 2.21)
169 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
170 | railties (7.2.2.1)
171 | actionpack (= 7.2.2.1)
172 | activesupport (= 7.2.2.1)
173 | irb (~> 1.13)
174 | rackup (>= 1.0.0)
175 | rake (>= 12.2)
176 | thor (~> 1.0, >= 1.2.2)
177 | zeitwerk (~> 2.6)
178 | rake (13.2.1)
179 | rbtree (0.4.6)
180 | rdoc (6.10.0)
181 | psych (>= 4.0.0)
182 | redcarpet (3.6.0)
183 | reline (0.6.0)
184 | io-console (~> 0.5)
185 | rspec (3.13.0)
186 | rspec-core (~> 3.13.0)
187 | rspec-expectations (~> 3.13.0)
188 | rspec-mocks (~> 3.13.0)
189 | rspec-core (3.13.2)
190 | rspec-support (~> 3.13.0)
191 | rspec-expectations (3.13.3)
192 | diff-lcs (>= 1.2.0, < 2.0)
193 | rspec-support (~> 3.13.0)
194 | rspec-mocks (3.13.2)
195 | diff-lcs (>= 1.2.0, < 2.0)
196 | rspec-support (~> 3.13.0)
197 | rspec-rails (7.1.0)
198 | actionpack (>= 7.0)
199 | activesupport (>= 7.0)
200 | railties (>= 7.0)
201 | rspec-core (~> 3.13)
202 | rspec-expectations (~> 3.13)
203 | rspec-mocks (~> 3.13)
204 | rspec-support (~> 3.13)
205 | rspec-support (3.13.2)
206 | securerandom (0.4.1)
207 | set (1.1.1)
208 | sorted_set (1.0.3)
209 | rbtree
210 | set (~> 1.0)
211 | stackprof (0.2.26)
212 | stringio (3.1.2)
213 | thor (1.3.2)
214 | timeout (0.4.3)
215 | tomparse (0.4.2)
216 | tzinfo (2.0.6)
217 | concurrent-ruby (~> 1.0)
218 | useragent (0.16.11)
219 | websocket-driver (0.7.6)
220 | websocket-extensions (>= 0.1.0)
221 | websocket-extensions (0.1.5)
222 | yard (0.9.37)
223 | yard-tomdoc (0.7.1)
224 | tomparse (>= 0.4.0)
225 | yard
226 | zeitwerk (2.7.1)
227 |
228 | PLATFORMS
229 | ruby
230 |
231 | DEPENDENCIES
232 | benchmark-ips
233 | curly-templates!
234 | genspec!
235 | github-markup
236 | rails (~> 7.2.0)
237 | rake
238 | redcarpet
239 | rspec (>= 3)
240 | rspec-rails
241 | stackprof
242 | yard
243 | yard-tomdoc
244 |
245 | BUNDLED WITH
246 | 2.5.17
247 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Unreleased
2 | * Add tests with Rails 7.2 and 8.0
3 | * Add tests with Ruby 3.4
4 | * Drop support for Ruby < 3.2
5 |
6 | ### Curly 3.4.0 (July 2, 2024)
7 | * Drop upper limit on Rails, test with Rails main.
8 | * Drop support for Ruby < 3.1.
9 | * Drop support for Rails < 6.1.
10 |
11 | ### Curly 3.3.0 (November 13, 2023)
12 | * Add support for Rails 7.1
13 |
14 | ### Curly 3.2.0 (June 1, 2023)
15 |
16 | * Add support for Ruby 3.2
17 | * Drop support for Ruby 2.6
18 | * Drop support for Rails 4.2
19 |
20 | ### Curly 3.1.0 (November, 21, 2022)
21 |
22 | * Add support for Ruby 3.0 & 3.1
23 | * Add support for Rails 7.0
24 |
25 | ### Curly 3.0.0 (January 19, 2021)
26 |
27 | * Add support for Rails 6.0 and 6.1.
28 | * Remove support for Rails versions below 4.2.
29 |
30 | ### Curly 2.6.5 (May 23, 2018)
31 |
32 | * Add support for Rails 5.2.
33 |
34 | *Benjamin Quorning*
35 |
36 | ### Curly 2.6.4 (April 28, 2017)
37 |
38 | * Add support for Rails 5.1.
39 |
40 | *Benjamin Quorning*
41 |
42 | ### Curly 2.6.3 (March 24, 2017)
43 |
44 | * Added generator for Rails' built in scaffold command.
45 | * Added `curly:install` generator for creating layout files.
46 |
47 | *Jack M*
48 |
49 | ### Curly 2.6.2 (December 22, 2016)
50 |
51 | * Change `DependencyTracker.call` to returns array, for compatibility with
52 | Rails 5.0.
53 |
54 | *Benjamin Quorning*
55 |
56 | ### Curly 2.6.1 (August 3, 2016)
57 |
58 | * Use Rails' `constantize` method instead of `get_const` when looking up
59 | presenter classes, so that Rails has a chance to autoload missing classes.
60 |
61 | *Creighton Long*
62 |
63 | ### Curly 2.6.0 (July 4, 2016)
64 |
65 | * Add support for Rails 5.
66 |
67 | * Add support for arbitrary component attributes. If the presenter method accepts
68 | arbitrary keyword arguments, the corresponding component is allowed to pass
69 | any attribute it wants.
70 |
71 | *Jeremy Rodi*
72 |
73 | * Add support for testing presenters with RSpec:
74 |
75 | ```ruby\
76 | require 'curly/rspec'
77 |
78 | # spec/presenters/posts/show_presenter_spec.rb
79 | describe Posts::ShowPresenter, type: :presenter do
80 | describe "#body" do
81 | it "renders the post's body as Markdown" do
82 | assign(:post, double(:post, body: "**hello!**"))
83 | expect(presenter.body).to eq "
hello!"
84 | end
85 | end
86 | end
87 | ```
88 |
89 | *Daniel Schierbeck*
90 |
91 | ### Curly 2.5.0 (May 19, 2015)
92 |
93 | * Allow passing a block as the `default:` option to `presents`.
94 |
95 | ```ruby
96 | class CommentPresenter < Curly::Presenter
97 | presents :comment
98 | presents(:author) { @comment.author }
99 | end
100 | ```
101 |
102 | *Steven Davidovitz & Jeremy Rodi*
103 |
104 | ### Curly 2.4.0 (February 24, 2015)
105 |
106 | * Add an `exposes_helper` class methods to Curly::Presenter. This allows exposing
107 | helper methods as components.
108 |
109 | *Jeremy Rodi*
110 |
111 | * Add a shorthand syntax for using components within a context. This allows you
112 | to write `{{author:name}}` rather than `{{@author}}{{name}}{{/author}}`.
113 |
114 | *Daniel Schierbeck*
115 |
116 | ### Curly 2.3.2 (January 13, 2015)
117 |
118 | * Fix an issue that caused presenter parameters to get mixed up.
119 |
120 | *Cristian Planas*
121 |
122 | * Clean up the testing code.
123 |
124 | *Daniel Schierbeck*
125 |
126 | ### Curly 2.3.1 (January 7, 2015)
127 |
128 | * Fix an issue with nested context blocks.
129 |
130 | *Daniel Schierbeck*
131 |
132 | * Make `respond_to_missing?` work with presenter objects.
133 |
134 | *Jeremy Rodi*
135 |
136 | ### Curly 2.3.0 (December 22, 2014)
137 |
138 | * Add support for Rails 4.2.
139 |
140 | *Łukasz Niemier*
141 |
142 | * Allow spaces within components.
143 |
144 | *Łukasz Niemier*
145 |
146 | ### Curly 2.2.0 (December 4, 2014)
147 |
148 | * Allow configuring arbitrary cache options.
149 |
150 | *Daniel Schierbeck*
151 |
152 | ### Curly 2.1.1 (November 12, 2014)
153 |
154 | * Fix a bug where a parent presenter's parameters were not being passed to the
155 | child presenter when using context blocks.
156 |
157 | *Daniel Schierbeck*
158 |
159 | ### Curly 2.1.0 (November 6, 2014)
160 |
161 | * Add support for [context blocks](https://github.com/zendesk/curly#context-blocks).
162 |
163 | *Daniel Schierbeck*
164 |
165 | * Forward the parent presenter's parameters to the nested presenter when
166 | rendering collection blocks.
167 |
168 | *Daniel Schierbeck*
169 |
170 | ### Curly 2.0.1 (September 9, 2014)
171 |
172 | * Fixed an issue when using Curly with Rails 4.1.
173 |
174 | *Daniel Schierbeck*
175 |
176 | * Add line number information to syntax errors.
177 |
178 | *Jeremy Rodi*
179 |
180 | ### Curly 2.0.0 (July 1, 2014)
181 |
182 | * Rename Curly::CompilationError to Curly::PresenterNotFound.
183 |
184 | *Daniel Schierbeck*
185 |
186 | ### Curly 2.0.0.beta1 (June 27, 2014)
187 |
188 | * Add support for collection blocks.
189 |
190 | *Daniel Schierbeck*
191 |
192 | * Add support for keyword parameters to references.
193 |
194 | *Alisson Cavalcante Agiani, Jeremy Rodi, and Daniel Schierbeck*
195 |
196 | * Remove memory leak that could cause unbounded memory growth.
197 |
198 | *Daniel Schierbeck*
199 |
200 | ### Curly 1.0.0rc1 (February 18, 2014)
201 |
202 | * Add support for conditional blocks:
203 |
204 | ```
205 | {{#admin?}}
206 | Hello!
207 | {{/admin?}}
208 | ```
209 |
210 | *Jeremy Rodi*
211 |
212 | ### Curly 0.12.0 (December 3, 2013)
213 |
214 | * Allow Curly to output Curly syntax by using the `{{{ ... }}` syntax:
215 |
216 | ```
217 | {{{curly_example}}
218 | ```
219 |
220 | *Daniel Schierbeck and Benjamin Quorning*
221 |
222 | ### Curly 0.11.0 (July 31, 2013)
223 |
224 | * Make Curly raise an exception when a reference or comment is not closed.
225 |
226 | *Daniel Schierbeck*
227 |
228 | * Fix a bug that caused an infinite loop when there was whitespace in a reference.
229 |
230 | *Daniel Schierbeck*
231 |
232 | ### Curly 0.10.2 (July 11, 2013)
233 |
234 | * Fix a bug that caused non-string presenter method return values to be
235 | discarded.
236 |
237 | *Daniel Schierbeck*
238 |
239 | ### Curly 0.10.1 (July 11, 2013)
240 |
241 | * Fix a bug in the compiler that caused some templates to be erroneously HTML
242 | escaped.
243 |
244 | *Daniel Schierbeck*
245 |
246 | ### Curly 0.10.0 (July 11, 2013)
247 |
248 | * Allow comments in Curly templates using the `{{! ... }}` syntax:
249 |
250 | ```
251 | {{! This is a comment }}
252 | ```
253 |
254 | *Daniel Schierbeck*
255 |
256 | ### Curly 0.9.1 (June 20, 2013)
257 |
258 | * Better error handling. If a presenter class cannot be found, we not raise a
259 | more descriptive exception.
260 |
261 | *Daniel Schierbeck*
262 |
263 | * Include the superclass' dependencies in a presenter's dependency list.
264 |
265 | *Daniel Schierbeck*
266 |
267 | ### Curly 0.9.0 (June 4, 2013)
268 |
269 | * Allow running setup code before rendering a Curly view. Simply add a `#setup!`
270 | method to your presenter – it will be called by Curly just before the view is
271 | rendered.
272 |
273 | *Daniel Schierbeck*
274 |
--------------------------------------------------------------------------------
/spec/compiler/collections_spec.rb:
--------------------------------------------------------------------------------
1 | describe Curly::Compiler do
2 | include CompilationSupport
3 |
4 | context "normal rendering" do
5 | before do
6 | define_presenter "ItemPresenter" do
7 | presents :item
8 | delegate :name, to: :@item
9 | end
10 | end
11 |
12 | it "compiles collection blocks" do
13 | define_presenter do
14 | presents :items
15 | attr_reader :items
16 | end
17 |
18 | item1 = double("item1", name: "foo")
19 | item2 = double("item2", name: "bar")
20 |
21 | template = "
{{*items}}- {{name}}
{{/items}}
"
22 | expect(render(template, locals: { items: [item1, item2] })).
23 | to eql "
"
24 | end
25 |
26 | it "allows attributes on collection blocks" do
27 | define_presenter do
28 | presents :items
29 |
30 | def items(status: nil)
31 | if status
32 | @items.select {|item| item.status == status }
33 | else
34 | @items
35 | end
36 | end
37 | end
38 |
39 | item1 = double("item1", name: "foo", status: "active")
40 | item2 = double("item2", name: "bar", status: "inactive")
41 |
42 | template = "
{{*items status=active}}- {{name}}
{{/items}}
"
43 | expect(render(template, locals: { items: [item1, item2] })).
44 | to eql "
"
45 | end
46 |
47 | it "fails if the component doesn't support enumeration" do
48 | template = "
{{*numbers}}- {{name}}
{{/numbers}}
"
49 | expect { render(template) }.to raise_exception(Curly::Error)
50 | end
51 |
52 | it "works even if the component method doesn't return an Array" do
53 | define_presenter do
54 | def companies
55 | "Arla"
56 | end
57 | end
58 |
59 | define_presenter "CompanyPresenter" do
60 | presents :company
61 |
62 | def name
63 | @company
64 | end
65 | end
66 |
67 | template = "
{{*companies}}- {{name}}
{{/companies}}
"
68 | expect(render(template)).to eql "
"
69 | end
70 |
71 | it "passes the index of the current item to the nested presenter" do
72 | define_presenter do
73 | presents :items
74 | attr_reader :items
75 | end
76 |
77 | define_presenter "ItemPresenter" do
78 | presents :item_counter
79 |
80 | def index
81 | @item_counter
82 | end
83 | end
84 |
85 | item1 = double("item1")
86 | item2 = double("item2")
87 |
88 | template = "
{{*items}}- {{index}}
{{/items}}
"
89 | expect(render(template, locals: { items: [item1, item2] })).
90 | to eql "
"
91 | end
92 |
93 | it "restores the previous scope after exiting the collection block" do
94 | define_presenter do
95 | presents :items
96 | attr_reader :items
97 |
98 | def title
99 | "Inventory"
100 | end
101 | end
102 |
103 | define_presenter "ItemPresenter" do
104 | presents :item
105 | delegate :name, :parts, to: :@item
106 | end
107 |
108 | define_presenter "PartPresenter" do
109 | presents :part
110 | delegate :identifier, to: :@part
111 | end
112 |
113 | part = double("part", identifier: "X")
114 | item = double("item", name: "foo", parts: [part])
115 |
116 | template = "{{*items}}{{*parts}}{{identifier}}{{/parts}}{{name}}{{/items}}{{title}}"
117 | expect(render(template, locals: { items: [item] })).
118 | to eql "XfooInventory"
119 | end
120 |
121 | it "passes the parent presenter's options to the nested presenter" do
122 | define_presenter do
123 | presents :items, :prefix
124 | attr_reader :items
125 | end
126 |
127 | define_presenter "ItemPresenter" do
128 | presents :item, :prefix
129 | delegate :name, to: :@item
130 | attr_reader :prefix
131 | end
132 |
133 | item1 = double(name: "foo")
134 | item2 = double(name: "bar")
135 |
136 | template = "{{*items}}{{prefix}}: {{name}}; {{/items}}"
137 | expect(render(template, locals: { prefix: "SKU", items: [item1, item2] })).
138 | to eql "SKU: foo; SKU: bar; "
139 | end
140 |
141 | it "compiles nested collection blocks" do
142 | define_presenter do
143 | presents :items
144 | attr_reader :items
145 | end
146 |
147 | define_presenter "ItemPresenter" do
148 | presents :item
149 | delegate :name, :parts, to: :@item
150 | end
151 |
152 | define_presenter "PartPresenter" do
153 | presents :part
154 | delegate :identifier, to: :@part
155 | end
156 |
157 | item1 = double("item1", name: "item1", parts: [double(identifier: "A"), double(identifier: "B")])
158 | item2 = double("item2", name: "item2", parts: [double(identifier: "C"), double(identifier: "D")])
159 |
160 | template = "{{*items}}{{name}}: {{*parts}}{{identifier}}{{/parts}}; {{/items}}"
161 | expect(render(template, locals: { items: [item1, item2] })).
162 | to eql "item1: AB; item2: CD; "
163 | end
164 | end
165 |
166 | context "re-using assign names" do
167 | before do
168 | define_presenter do
169 | presents :comment
170 |
171 | attr_reader :comment
172 |
173 | def comments
174 | ["yolo", "xoxo"]
175 | end
176 |
177 | def comment(&block)
178 | block.call("foo!")
179 | end
180 |
181 | def form(&block)
182 | block.call
183 | end
184 | end
185 |
186 | define_presenter "CommentPresenter" do
187 | presents :comment
188 | end
189 |
190 | define_presenter "FormPresenter" do
191 | presents :comment
192 | attr_reader :comment
193 | end
194 | end
195 |
196 | it "allows re-using assign names in collection blocks" do
197 | options = { "comment" => "first post!" }
198 | template = "{{*comments}}{{/comments}}{{@form}}{{comment}}{{/form}}"
199 | expect(render(template, locals: options)).to eql "first post!"
200 | end
201 |
202 | it "allows re-using assign names in context blocks" do
203 | options = { "comment" => "first post!" }
204 | template = "{{@comment}}{{/comment}}{{@form}}{{comment}}{{/form}}"
205 | expect(render(template, locals: options)).to eql "first post!"
206 | end
207 | end
208 |
209 | context "using namespaced names" do
210 | before do
211 | define_presenter "Layouts::ShowPresenter" do
212 | def comments
213 | ["hello", "world"]
214 | end
215 | end
216 |
217 | define_presenter "Layouts::ShowPresenter::CommentPresenter" do
218 | presents :comment
219 |
220 | attr_reader :comment
221 | end
222 | end
223 |
224 | it "renders correctly" do
225 | template = "{{*comments}}{{comment}} {{/comments}}"
226 | expect(render(template, presenter: "Layouts::ShowPresenter")).
227 | to eql "hello world "
228 | end
229 | end
230 | end
231 |
--------------------------------------------------------------------------------
/spec/presenter_spec.rb:
--------------------------------------------------------------------------------
1 | describe Curly::Presenter do
2 | class CircusPresenter < Curly::Presenter
3 | module MonkeyComponents
4 | def monkey
5 | end
6 | end
7 |
8 | exposes_helper :foo
9 |
10 | include MonkeyComponents
11 |
12 | presents :midget, :clown, default: nil
13 | presents :elephant, default: "Dumbo"
14 | presents :puma, default: -> { 'block' }
15 | presents(:lion) { @elephant.upcase }
16 | presents(:something) { self }
17 |
18 | attr_reader :midget, :clown, :elephant, :puma, :lion, :something
19 | end
20 |
21 | class FrenchCircusPresenter < CircusPresenter
22 | presents :elephant, default: "Babar"
23 | end
24 |
25 | class FancyCircusPresenter < CircusPresenter
26 | presents :champagne
27 | end
28 |
29 | class CircusPresenter::MonkeyPresenter < Curly::Presenter
30 | end
31 |
32 | module PresenterContainer
33 | class NestedPresenter < Curly::Presenter
34 | end
35 | module PresenterSubcontainer
36 | class SubNestedPresenter < Curly::Presenter
37 | end
38 | end
39 | end
40 |
41 | describe "#initialize" do
42 | let(:context) { double("context") }
43 |
44 | it "sets the presented identifiers as instance variables" do
45 | presenter = CircusPresenter.new(context,
46 | midget: "Meek Harolson",
47 | clown: "Bubbles"
48 | )
49 |
50 | expect(presenter.midget).to eq "Meek Harolson"
51 | expect(presenter.clown).to eq "Bubbles"
52 | end
53 |
54 | it "raises an exception if a required identifier is not specified" do
55 | expect {
56 | FancyCircusPresenter.new(context, {})
57 | }.to raise_exception(ArgumentError, "required identifier `champagne` missing")
58 | end
59 |
60 | it "allows specifying default values for identifiers" do
61 | # Make sure subclasses can change default values.
62 | french_presenter = FrenchCircusPresenter.new(context)
63 | expect(french_presenter.elephant).to eq "Babar"
64 | expect(french_presenter.lion).to eq 'BABAR'
65 | expect(french_presenter.puma).to be_a Proc
66 |
67 | # The subclass shouldn't change the superclass' defaults, though.
68 | presenter = CircusPresenter.new(context)
69 | expect(presenter.elephant).to eq "Dumbo"
70 | expect(presenter.lion).to eq 'DUMBO'
71 | expect(presenter.puma).to be_a Proc
72 | end
73 |
74 | it "doesn't call a block if given as a value for identifiers" do
75 | lion = proc { 'Simba' }
76 | presenter = CircusPresenter.new(context, lion: lion)
77 | expect(presenter.lion).to be lion
78 | end
79 |
80 | it "calls default blocks in the instance of the presenter" do
81 | presenter = CircusPresenter.new(context)
82 | expect(presenter.something).to be presenter
83 | end
84 | end
85 |
86 | describe "#method_missing" do
87 | let(:context) { double("context") }
88 | subject {
89 | CircusPresenter.new(context,
90 | midget: "Meek Harolson",
91 | clown: "Bubbles")
92 | }
93 |
94 | it "delegates calls to the context" do
95 | expect(context).to receive(:undefined).once
96 | subject.undefined
97 | end
98 |
99 | it "allows method calls on context-defined methods" do
100 | expect(context).to receive(:respond_to?).
101 | with(:undefined, false).once.and_return(true)
102 | subject.method(:undefined)
103 | end
104 | end
105 |
106 | describe ".exposes_helper" do
107 | let(:context) { double("context") }
108 | subject {
109 | CircusPresenter.new(context,
110 | midget: "Meek Harolson",
111 | clown: "Bubbles")
112 | }
113 |
114 | it "allows a method as a component" do
115 | CircusPresenter.component_available?(:foo)
116 | end
117 |
118 | it "delegates the call to the context" do
119 | expect(context).to receive(:foo).once
120 | expect(subject).not_to receive(:method_missing)
121 | subject.foo
122 | end
123 |
124 | it "doesn't delegate other calls to the context" do
125 | expect { subject.bar }.to raise_error RSpec::Mocks::MockExpectationError
126 | end
127 | end
128 |
129 | describe ".presenter_for_path" do
130 | it "returns the presenter class for the given path" do
131 | presenter = double("presenter")
132 | stub_const("Foo::BarPresenter", presenter)
133 |
134 | expect(Curly::Presenter.presenter_for_path("foo/bar")).to eq presenter
135 | end
136 |
137 | it "returns nil if there is no presenter for the given path" do
138 | expect(Curly::Presenter.presenter_for_path("foo/bar")).to be_nil
139 | end
140 | end
141 |
142 | describe ".presenter_for_name" do
143 | it 'looks through the container namespaces' do
144 | expect(PresenterContainer::PresenterSubcontainer::SubNestedPresenter.presenter_for_name('nested')).to eq PresenterContainer::NestedPresenter
145 | end
146 |
147 | it 'looks through the container namespaces' do
148 | expect(Curly::Presenter.presenter_for_name('presenter_container/presenter_subcontainer/nested', [])).to eq(PresenterContainer::NestedPresenter)
149 | end
150 |
151 | it "returns the presenter class for the given name" do
152 | expect(CircusPresenter.presenter_for_name("monkey")).to eq CircusPresenter::MonkeyPresenter
153 | end
154 |
155 | it "looks in the namespace" do
156 | expect(CircusPresenter.presenter_for_name("french_circus")).to eq FrenchCircusPresenter
157 | end
158 |
159 | it "returns Curly::PresenterNameError if the presenter class doesn't exist" do
160 | expect { CircusPresenter.presenter_for_name("clown") }.to raise_exception(Curly::PresenterNameError)
161 | end
162 | end
163 |
164 | describe ".available_components" do
165 | it "includes the methods on the presenter" do
166 | expect(CircusPresenter.available_components).to include("midget")
167 | end
168 |
169 | it "does not include methods on the Curly::Presenter base class" do
170 | expect(CircusPresenter.available_components).not_to include("cache_key")
171 | end
172 | end
173 |
174 | describe ".component_available?" do
175 | it "returns true if the method is available" do
176 | expect(CircusPresenter.component_available?("midget")).to eq true
177 | end
178 |
179 | it "returns false if the method is not available" do
180 | expect(CircusPresenter.component_available?("bear")).to eq false
181 | end
182 | end
183 |
184 | describe ".version" do
185 | it "sets the version of the presenter" do
186 | presenter1 = Class.new(Curly::Presenter) do
187 | version 42
188 | end
189 |
190 | presenter2 = Class.new(Curly::Presenter) do
191 | version 1337
192 | end
193 |
194 | expect(presenter1.version).to eq 42
195 | expect(presenter2.version).to eq 1337
196 | end
197 |
198 | it "returns 0 if no version has been set" do
199 | presenter = Class.new(Curly::Presenter)
200 | expect(presenter.version).to eq 0
201 | end
202 | end
203 |
204 | describe ".cache_key" do
205 | it "includes the presenter's class name and version" do
206 | presenter = Class.new(Curly::Presenter) { version 42 }
207 | stub_const("Foo::BarPresenter", presenter)
208 |
209 | expect(Foo::BarPresenter.cache_key).to eq "Foo::BarPresenter/42"
210 | end
211 |
212 | it "includes the cache keys of presenters in the dependency list" do
213 | presenter = Class.new(Curly::Presenter) do
214 | version 42
215 | depends_on 'foo/bum'
216 | end
217 |
218 | dependency = Class.new(Curly::Presenter) do
219 | version 1337
220 | end
221 |
222 | stub_const("Foo::BarPresenter", presenter)
223 | stub_const("Foo::BumPresenter", dependency)
224 |
225 | cache_key = Foo::BarPresenter.cache_key
226 | expect(cache_key).to eq "Foo::BarPresenter/42/Foo::BumPresenter/1337"
227 | end
228 |
229 | it "uses the view path of a dependency if there is no presenter for it" do
230 | presenter = Class.new(Curly::Presenter) do
231 | version 42
232 | depends_on 'foo/bum'
233 | end
234 |
235 | stub_const("Foo::BarPresenter", presenter)
236 |
237 | cache_key = Foo::BarPresenter.cache_key
238 | expect(cache_key).to eq "Foo::BarPresenter/42/foo/bum"
239 | end
240 | end
241 |
242 | describe ".dependencies" do
243 | it "returns the dependencies defined for the presenter" do
244 | presenter = Class.new(Curly::Presenter) { depends_on 'foo' }
245 | expect(presenter.dependencies.to_a).to eq ['foo']
246 | end
247 |
248 | it "includes the dependencies defined for parent classes" do
249 | Curly::Presenter.dependencies
250 | parent = Class.new(Curly::Presenter) { depends_on 'foo' }
251 | presenter = Class.new(parent) { depends_on 'bar' }
252 | expect(presenter.dependencies.to_a).to match_array ['foo', 'bar']
253 | end
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/lib/curly/presenter.rb:
--------------------------------------------------------------------------------
1 | require 'curly/presenter_name_error'
2 | require "sorted_set"
3 |
4 | module Curly
5 |
6 | # A base class that can be subclassed by concrete presenters.
7 | #
8 | # A Curly presenter is responsible for delivering data to templates, in the
9 | # form of simple strings. Each public instance method on the presenter class
10 | # can be referenced in a template. When a template is evaluated with a
11 | # presenter, the referenced methods will be called with no arguments, and
12 | # the returned strings inserted in place of the components in the template.
13 | #
14 | # Note that strings that are not HTML safe will be escaped.
15 | #
16 | # A presenter is always instantiated with a context to which it delegates
17 | # unknown messages, usually an instance of ActionView::Base provided by
18 | # Rails. See Curly::TemplateHandler for a typical use.
19 | #
20 | # Examples
21 | #
22 | # class BlogPresenter < Curly::Presenter
23 | # presents :post
24 | #
25 | # def title
26 | # @post.title
27 | # end
28 | #
29 | # def body
30 | # markdown(@post.body)
31 | # end
32 | #
33 | # def author
34 | # @post.author.full_name
35 | # end
36 | # end
37 | #
38 | # presenter = BlogPresenter.new(context, post: post)
39 | # presenter.author #=> "Jackie Chan"
40 | #
41 | class Presenter
42 |
43 | # Initializes the presenter with the given context and options.
44 | #
45 | # context - An ActionView::Base context.
46 | # options - A Hash of options given to the presenter.
47 | def initialize(context, options = {})
48 | @_context = context
49 | options.stringify_keys!
50 |
51 | self.class.presented_names.each do |name|
52 | value = options.fetch(name) do
53 | default_values.fetch(name) do
54 | block = default_blocks.fetch(name) do
55 | raise ArgumentError.new("required identifier `#{name}` missing")
56 | end
57 |
58 | instance_exec(name, &block)
59 | end
60 | end
61 |
62 | instance_variable_set("@#{name}", value)
63 | end
64 | end
65 |
66 | # Sets up the view.
67 | #
68 | # Override this method in your presenter in order to do setup before the
69 | # template is rendered. One use case is to call `content_for` in order
70 | # to inject content into other templates, e.g. a layout.
71 | #
72 | # Examples
73 | #
74 | # class Posts::ShowPresenter < Curly::Presenter
75 | # presents :post
76 | #
77 | # def setup!
78 | # content_for :page_title, @post.title
79 | # end
80 | # end
81 | #
82 | # Returns nothing.
83 | def setup!
84 | # Does nothing.
85 | end
86 |
87 | # The key that should be used to cache the view.
88 | #
89 | # Unless `#cache_key` returns nil, the result of rendering the template
90 | # that the presenter supports will be cached. The return value will be
91 | # part of the final cache key, along with a digest of the template itself.
92 | #
93 | # Any object can be used as a cache key, so long as it
94 | #
95 | # - is a String,
96 | # - responds to #cache_key itself, or
97 | # - is an Array or a Hash whose items themselves fit either of these
98 | # criteria.
99 | #
100 | # Returns the cache key Object or nil if no caching should be performed.
101 | def cache_key
102 | nil
103 | end
104 |
105 | # The options that should be passed to the cache backend when caching the
106 | # view. The exact options may vary depending on the backend you're using.
107 | #
108 | # The most common option is `:expires_in`, which controls the duration of
109 | # time that the cached view should be considered fresh. Because it's so
110 | # common, you can set that option simply by defining `#cache_duration`.
111 | #
112 | # Note: if you set the `:expires_in` option through this method, the
113 | # `#cache_duration` value will be ignored.
114 | #
115 | # Returns a Hash.
116 | def cache_options
117 | {}
118 | end
119 |
120 | # The duration that the view should be cached for. Only relevant if
121 | # `#cache_key` returns a non nil value.
122 | #
123 | # If nil, the view will not have an expiration time set. See also
124 | # `#cache_options` for a more flexible way to set cache options.
125 | #
126 | # Examples
127 | #
128 | # def cache_duration
129 | # 10.minutes
130 | # end
131 | #
132 | # Returns the Fixnum duration of the cache item, in seconds, or nil if no
133 | # duration should be set.
134 | def cache_duration
135 | nil
136 | end
137 |
138 | class << self
139 |
140 | # The name of the presenter class for a given view path.
141 | #
142 | # path - The String path of a view.
143 | #
144 | # Examples
145 | #
146 | # Curly::TemplateHandler.presenter_name_for_path("foo/bar")
147 | # #=> "Foo::BarPresenter"
148 | #
149 | # Returns the String name of the matching presenter class.
150 | def presenter_name_for_path(path)
151 | "#{path}_presenter".camelize
152 | end
153 |
154 | # Returns the presenter class for the given path.
155 | #
156 | # path - The String path of a template.
157 | #
158 | # Returns the Class or nil if the constant cannot be found.
159 | def presenter_for_path(path)
160 | begin
161 | # Assume that the path can be derived without a prefix; In other words
162 | # from the given path we can look up objects by namespace.
163 | presenter_for_name(path.camelize, [])
164 | rescue Curly::PresenterNameError
165 | nil
166 | end
167 | end
168 |
169 | # Retrieve the named presenter with consideration for object scope.
170 | # The namespace_prefixes are to acknowledge that sometimes we will have
171 | # a subclass of Curly::Presenter receiving the .presenter_for_name
172 | # and other times we will not (when we are receiving this message by
173 | # way of the .presenter_for_path method).
174 | def presenter_for_name(name, namespace_prefixes = to_s.split('::'))
175 | full_class_name = name.camelcase << "Presenter"
176 | relative_namespace = full_class_name.split("::")
177 | class_name = relative_namespace.pop
178 | namespace = namespace_prefixes + relative_namespace
179 |
180 | # Because Rails' autoloading mechanism doesn't work properly with
181 | # namespace we need to loop through the namespace ourselves. Ideally,
182 | # `X::Y.const_get("Z")` would autoload `X::Z`, but only `X::Y::Z` is
183 | # attempted by Rails. This sucks, and hopefully we can find a better
184 | # solution in the future.
185 | begin
186 | full_name = namespace.join("::") << "::" << class_name
187 | full_name.constantize
188 | rescue NameError => e
189 | # Due to the way the exception hirearchy works, we need to check
190 | # that this exception is actually a `NameError` - since other
191 | # classes can inherit `NameError`, rescue will actually rescue
192 | # those classes as being under `NameError`, causing this block to
193 | # be executed for classes that aren't `NameError`s (but are rather
194 | # subclasses of it), which isn't the desired behavior. This
195 | # prevents anything but `NameError`s from triggering the resulting
196 | # code. `NoMethodError` is actually a subclass of `NameError`,
197 | # so a typo in a file (e.g. `present` instead of `presents`) can
198 | # cause the library to act as if the class was never defined.
199 | raise unless e.class == NameError
200 | if namespace.empty?
201 | raise Curly::PresenterNameError.new(e, name)
202 | end
203 | namespace.pop
204 | retry
205 | end
206 | end
207 |
208 | # Whether a component is available to templates rendered with the
209 | # presenter.
210 | #
211 | # Templates have components which correspond with methods defined on
212 | # the presenter. By default, only public instance methods can be
213 | # referenced, and any method defined on Curly::Presenter itself cannot be
214 | # referenced. This means that methods such as `#cache_key` and #inspect
215 | # are not available. This is done for safety purposes.
216 | #
217 | # This policy can be changed by overriding this method in your presenters.
218 | #
219 | # name - The String name of the component.
220 | #
221 | # Returns true if the method can be referenced by a template,
222 | # false otherwise.
223 | def component_available?(name)
224 | available_components.include?(name)
225 | end
226 |
227 | # A list of components available to templates rendered with the presenter.
228 | #
229 | # Returns an Array of String component names.
230 | def available_components
231 | @_available_components ||= begin
232 | methods = public_instance_methods - Curly::Presenter.public_instance_methods
233 | methods.map(&:to_s)
234 | end
235 | end
236 |
237 | # The set of view paths that the presenter depends on.
238 | #
239 | # Examples
240 | #
241 | # class Posts::ShowPresenter < Curly::Presenter
242 | # version 2
243 | # depends_on 'posts/comment', 'posts/comment_form'
244 | # end
245 | #
246 | # Posts::ShowPresenter.dependencies
247 | # #=> ['posts/comment', 'posts/comment_form']
248 | #
249 | # Returns a Set of String view paths.
250 | def dependencies
251 | # The base presenter doesn't have any dependencies.
252 | return SortedSet.new if self == Curly::Presenter
253 |
254 | @dependencies ||= SortedSet.new
255 | @dependencies.union(superclass.dependencies)
256 | end
257 |
258 | # Indicate that the presenter depends a list of other views.
259 | #
260 | # deps - A list of String view paths that the presenter depends on.
261 | #
262 | # Returns nothing.
263 | def depends_on(*dependencies)
264 | @dependencies ||= SortedSet.new
265 | @dependencies.merge(dependencies)
266 | end
267 |
268 | # Get or set the version of the presenter.
269 | #
270 | # version - The Integer version that should be set. If nil, no version
271 | # is set.
272 | #
273 | # Returns the current Integer version of the presenter.
274 | def version(version = nil)
275 | @version = version if version.present?
276 | @version || 0
277 | end
278 |
279 | # The cache key for the presenter class. Includes all dependencies as
280 | # well.
281 | #
282 | # Returns a String cache key.
283 | def cache_key
284 | @cache_key ||= compute_cache_key
285 | end
286 |
287 | private
288 |
289 | def compute_cache_key
290 | dependency_cache_keys = dependencies.map do |path|
291 | if presenter = presenter_for_path(path)
292 | presenter.cache_key
293 | else
294 | path
295 | end
296 | end
297 |
298 | [name, version, dependency_cache_keys].flatten.join("/")
299 | end
300 |
301 | def presents(*args, **options, &block)
302 | if options.key?(:default) && block_given?
303 | raise ArgumentError, "Cannot provide both `default:` and block"
304 | end
305 |
306 | self.presented_names += args.map(&:to_s)
307 |
308 | if options.key?(:default)
309 | args.each do |arg|
310 | self.default_values = default_values.merge(arg.to_s => options[:default]).freeze
311 | end
312 | end
313 |
314 | if block_given?
315 | args.each do |arg|
316 | self.default_blocks = default_blocks.merge(arg.to_s => block).freeze
317 | end
318 | end
319 | end
320 |
321 | def exposes_helper(*methods)
322 | methods.each do |method_name|
323 | define_method(method_name) do |*args|
324 | @_context.public_send(method_name, *args)
325 | end
326 | end
327 | end
328 |
329 | alias_method :exposes_helpers, :exposes_helper
330 | end
331 |
332 | private
333 |
334 | class_attribute :presented_names, :default_values, :default_blocks
335 |
336 | self.presented_names = [].freeze
337 | self.default_values = {}.freeze
338 | self.default_blocks = {}.freeze
339 |
340 | delegate :render, to: :@_context
341 |
342 | # Delegates private method calls to the current view context.
343 | #
344 | # The view context, an instance of ActionView::Base, is set by Rails.
345 | def method_missing(method, *args, &block)
346 | @_context.public_send(method, *args, &block)
347 | end
348 |
349 | # Tells ruby (and developers) what methods we can accept.
350 | def respond_to_missing?(method, include_private = false)
351 | @_context.respond_to?(method, false)
352 | end
353 | end
354 | end
355 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Curly
2 | =======
3 |
4 | Curly is a template language that completely separates structure and logic.
5 | Instead of interspersing your HTML with snippets of Ruby, all logic is moved
6 | to a presenter class.
7 |
8 |
9 | ### Table of Contents
10 |
11 | 1. [Installing](#installing)
12 | 2. [How to use Curly](#how-to-use-curly)
13 | 1. [Identifiers](#identifiers)
14 | 2. [Attributes](#attributes)
15 | 3. [Conditional blocks](#conditional-blocks)
16 | 4. [Collection blocks](#collection-blocks)
17 | 5. [Context blocks](#context-blocks)
18 | 6. [Setting up state](#setting-up-state)
19 | 7. [Escaping Curly syntax](#escaping-curly-syntax)
20 | 8. [Comments](#comments)
21 | 3. [Presenters](#presenters)
22 | 1. [Layouts and content blocks](#layouts-and-content-blocks)
23 | 2. [Rails helper methods](#rails-helper-methods)
24 | 3. [Testing](#testing)
25 | 4. [Examples](#examples)
26 | 4. [Caching](#caching)
27 |
28 |
29 | Installing
30 | ----------
31 |
32 | Installing Curly is as simple as running `gem install curly-templates`. If you're
33 | using Bundler to manage your dependencies, add this to your Gemfile
34 |
35 | ```ruby
36 | gem 'curly-templates'
37 | ```
38 |
39 | Curly can also install an application layout file, replacing the .erb file commonly
40 | created by Rails. If you wish to use this, run the `curly:install` generator.
41 |
42 | ```sh
43 | $ rails generate curly:install
44 | ```
45 |
46 |
47 | How to use Curly
48 | ----------------
49 |
50 | In order to use Curly for a view or partial, use the suffix `.curly` instead of
51 | `.erb`, e.g. `app/views/posts/_comment.html.curly`. Curly will look for a
52 | corresponding presenter class named `Posts::CommentPresenter`. By convention,
53 | these are placed in `app/presenters/`, so in this case the presenter would
54 | reside in `app/presenters/posts/comment_presenter.rb`. Note that presenters
55 | for partials are not prepended with an underscore.
56 |
57 | Add some HTML to the partial template along with some Curly components:
58 |
59 | ```html
60 |
61 |
72 | ```
73 |
74 | The presenter will be responsible for providing the data for the components. Add
75 | the necessary Ruby code to the presenter:
76 |
77 | ```ruby
78 | # app/presenters/posts/comment_presenter.rb
79 | class Posts::CommentPresenter < Curly::Presenter
80 | presents :comment
81 |
82 | def body
83 | SafeMarkdown.render(@comment.body)
84 | end
85 |
86 | def author_link
87 | link_to @comment.author.name, @comment.author, rel: "author"
88 | end
89 |
90 | def deletion_link
91 | link_to "Delete", @comment, method: :delete
92 | end
93 |
94 | def time_ago
95 | time_ago_in_words(@comment.created_at)
96 | end
97 |
98 | def author?
99 | @comment.author == current_user
100 | end
101 | end
102 | ```
103 |
104 | The partial can now be rendered like any other, e.g. by calling
105 |
106 | ```ruby
107 | render 'comment', comment: comment
108 | render comment
109 | render collection: post.comments
110 | ```
111 |
112 | Curly _components_ are surrounded by curly brackets, e.g. `{{hello}}`. They always map to a
113 | public method on the presenter class, in this case `#hello`. Methods ending in a question mark
114 | can be used for [conditional blocks](#conditional-blocks), e.g. `{{#admin?}} ... {{/admin?}}`.
115 |
116 | ### Identifiers
117 |
118 | Curly components can specify an _identifier_ using the so-called dot notation: `{{x.y.z}}`.
119 | This can be very useful if the data you're accessing is hierarchical in nature. One common
120 | example is I18n:
121 |
122 | ```html
123 |
{{i18n.homepage.header}}
124 | ```
125 |
126 | ```ruby
127 | # In the presenter, the identifier is passed as an argument to the method. The
128 | # argument will always be a String.
129 | def i18n(key)
130 | translate(key)
131 | end
132 | ```
133 |
134 | The identifier is separated from the component name with a dot. If the presenter method
135 | has a default value for the argument, the identifier is optional – otherwise it's mandatory.
136 |
137 |
138 | ### Attributes
139 |
140 | In addition to [an identifier](#identifiers), Curly components can be annotated
141 | with *attributes*. These are key-value pairs that affect how a component is rendered.
142 |
143 | The syntax is reminiscent of HTML:
144 |
145 | ```html
146 |
{{sidebar rows=3 width=200px title="I'm the sidebar!"}}
147 | ```
148 |
149 | The presenter method that implements the component must have a matching keyword argument:
150 |
151 | ```ruby
152 | def sidebar(rows: "1", width: "100px", title:); end
153 | ```
154 |
155 | All argument values will be strings. A compilation error will be raised if
156 |
157 | - an attribute is used in a component without a matching keyword argument being present
158 | in the method definition; or
159 | - a required keyword argument in the method definition is not set as an attribute in the
160 | component.
161 |
162 | You can define default values using Ruby's own syntax. Additionally, if the presenter
163 | method accepts arbitrary keyword arguments using the `**doublesplat` syntax then all
164 | attributes will be valid for the component, e.g.
165 |
166 | ```ruby
167 | def greetings(**names)
168 | names.map {|name, greeting| "#{name}: #{greeting}!" }.join("\n")
169 | end
170 | ```
171 |
172 | ```html
173 | {{greetings alice=hello bob=hi}}
174 |
175 | alice: hello!
176 | bob: hi!
177 | ```
178 |
179 | Note that since keyword arguments in Ruby are represented as Symbol objects, which are
180 | not garbage collected in Ruby versions less than 2.2, accepting arbitrary attributes
181 | represents a security vulnerability if your application allows untrusted Curly templates
182 | to be rendered. Only use this feature with trusted templates if you're not on Ruby 2.2
183 | yet.
184 |
185 |
186 | ### Conditional blocks
187 |
188 | If there is some content you only want rendered under specific circumstances, you can
189 | use _conditional blocks_. The `{{#admin?}}...{{/admin?}}` syntax will only render the
190 | content of the block if the `admin?` method on the presenter returns true, while the
191 | `{{^admin?}}...{{/admin?}}` syntax will only render the content if it returns false.
192 |
193 | Both forms can have an identifier: `{{#locale.en?}}...{{/locale.en?}}` will only
194 | render the block if the `locale?` method on the presenter returns true given the
195 | argument `"en"`. Here's how to implement that method in the presenter:
196 |
197 | ```ruby
198 | class SomePresenter < Curly::Presenter
199 | # Allows rendering content only if the locale matches a specified identifier.
200 | def locale?(identifier)
201 | current_locale == identifier
202 | end
203 | end
204 | ```
205 |
206 | Furthermore, attributes can be set on the block. These only need to be specified when
207 | opening the block, not when closing it:
208 |
209 | ```html
210 | {{#square? width=3 height=3}}
211 |
It's square!
212 | {{/square?}}
213 | ```
214 |
215 | Attributes work the same way as they do for normal components.
216 |
217 |
218 | ### Collection blocks
219 |
220 | Sometimes you want to render one or more items within the current template, and splitting
221 | out a separate template and rendering that in the presenter is too much overhead. You can
222 | instead define the template that should be used to render the items inline in the current
223 | template using the _collection block syntax_.
224 |
225 | Collection blocks are opened using an asterisk:
226 |
227 | ```html
228 | {{*comments}}
229 |
{{body}} ({{author_name}})
230 | {{/comments}}
231 | ```
232 |
233 | The presenter will need to expose the method `#comments`, which should return a collection
234 | of objects:
235 |
236 | ```ruby
237 | class Posts::ShowPresenter < Curly::Presenter
238 | presents :post
239 |
240 | def comments
241 | @post.comments
242 | end
243 | end
244 | ```
245 |
246 | The template within the collection block will be used to render each item, and it will
247 | be backed by a presenter named after the component – in this case, `comments`. The name
248 | will be singularized and Curly will try to find the presenter class in the following
249 | order:
250 |
251 | * `Posts::ShowPresenter::CommentPresenter`
252 | * `Posts::CommentPresenter`
253 | * `CommentPresenter`
254 |
255 | This allows you some flexibility with regards to how you want to organize these nested
256 | templates and presenters.
257 |
258 | Note that the nested template will *only* have access to the methods on the nested
259 | presenter, but all variables passed to the "parent" presenter will be forwarded to
260 | the nested presenter. In addition, the current item in the collection will be
261 | passed, as well as that item's index in the collection:
262 |
263 | ```ruby
264 | class Posts::CommentPresenter < Curly::Presenter
265 | presents :post, :comment, :comment_counter
266 |
267 | def number
268 | # `comment_counter` is automatically set to the item's index in the collection,
269 | # starting with 1.
270 | @comment_counter
271 | end
272 |
273 | def body
274 | @comment.body
275 | end
276 |
277 | def author_name
278 | @comment.author.name
279 | end
280 | end
281 | ```
282 |
283 | Collection blocks are an alternative to splitting out a separate template and rendering
284 | that from the presenter – which solution is best depends on your use case.
285 |
286 |
287 | ### Context blocks
288 |
289 | While collection blocks allow you to define the template that should be used to render
290 | items in a collection right within the parent template, **context blocks** allow you
291 | to define the template for an arbitrary context. This is very powerful, and can be used
292 | to define widget-style components and helpers, and provide an easy way to work with
293 | structured data. Let's say you have a comment form on your page, and you'd rather keep
294 | the template inline. A simple template could look like:
295 |
296 | ```html
297 |
298 |
{{title}}
299 | {{body}}
300 |
301 | {{@comment_form}}
302 |
Name: {{name_field}}
303 |
E-mail: {{email_field}}
304 | {{comment_field}}
305 |
306 | {{submit_button}}
307 | {{/comment_form}}
308 | ```
309 |
310 | Note that an `@` character is used to denote a context block. Like with
311 | [collection blocks](#collection-blocks), a separate presenter class is used within the
312 | block, and a simple convention is used to find it. The name of the context component
313 | (in this case, `comment_form`) will be camel cased, and the current presenter's namespace
314 | will be searched:
315 |
316 | ```ruby
317 | class PostPresenter < Curly::Presenter
318 | presents :post
319 | def title; @post.title; end
320 | def body; markdown(@post.body); end
321 |
322 | # A context block method *must* take a block argument. The return value
323 | # of the method will be used when rendering. Calling the block argument will
324 | # render the nested template. If you pass a value when calling the block
325 | # argument it will be passed to the presenter.
326 | def comment_form(&block)
327 | form_for(Comment.new, &block)
328 | end
329 |
330 | # The presenter name is automatically deduced.
331 | class CommentFormPresenter < Curly::Presenter
332 | # The value passed to the block argument will be passed in a parameter named
333 | # after the component.
334 | presents :comment_form
335 |
336 | # Any parameters passed to the parent presenter will be forwarded to this
337 | # presenter as well.
338 | presents :post
339 |
340 | def name_field
341 | @comment_form.text_field :name
342 | end
343 |
344 | # ...
345 | end
346 | end
347 | ```
348 |
349 | Context blocks were designed to work well with Rails' helper methods such as `form_for`
350 | and `content_tag`, but you can also work directly with the block. For instance, if you
351 | want to directly control the value that is passed to the nested presenter, you can call
352 | the `call` method on the block yourself:
353 |
354 | ```ruby
355 | def author(&block)
356 | content_tag :div, class: "author" do
357 | # The return value of `call` will be the result of rendering the nested template
358 | # with the argument. You can post-process the string if you want.
359 | block.call(@post.author)
360 | end
361 | end
362 | ```
363 |
364 | #### Context shorthand syntax
365 |
366 | If you find yourself opening a context block just in order to use a single component,
367 | e.g. `{{@author}}{{name}}{{/author}}`, you can use the _shorthand syntax_ instead:
368 | `{{author:name}}`. This works for all component types, e.g.
369 |
370 | ```html
371 | {{#author:admin?}}
372 |
The author is an admin!
373 | {{/author:admin?}}
374 | ```
375 |
376 | The syntax works for nested contexts as well, e.g. `{{comment:author:name}}`. Any
377 | identifier and attributes are passed to the target component, which in this example
378 | would be `{{name}}`.
379 |
380 |
381 | ### Setting up state
382 |
383 | Although most code in Curly presenters should be free of side effects, sometimes side
384 | effects are required. One common example is defining content for a `content_for` block.
385 |
386 | If a Curly presenter class defines a `setup!` method, it will be called before the view
387 | is rendered:
388 |
389 | ```ruby
390 | class PostPresenter < Curly::Presenter
391 | presents :post
392 |
393 | def setup!
394 | content_for :title, post.title
395 |
396 | content_for :sidebar do
397 | render 'post_sidebar', post: post
398 | end
399 | end
400 | end
401 | ```
402 |
403 | ### Escaping Curly syntax
404 |
405 | In order to have `{{` appear verbatim in the rendered HTML, use the triple Curly escape syntax:
406 |
407 | ```
408 | This is {{{escaped}}.
409 | ```
410 |
411 | You don't need to escape the closing `}}`.
412 |
413 |
414 | ### Comments
415 |
416 | If you want to add comments to your Curly templates that are not visible in the rendered HTML,
417 | use the following syntax:
418 |
419 | ```html
420 | {{! This is some interesting stuff }}
421 | ```
422 |
423 |
424 | Presenters
425 | ----------
426 |
427 | Presenters are classes that inherit from `Curly::Presenter` – they're usually placed in
428 | `app/presenters/`, but you can put them anywhere you'd like. The name of the presenter
429 | classes match the virtual path of the view they're part of, so if your controller is
430 | rendering `posts/show`, the `Posts::ShowPresenter` class will be used. Note that Curly
431 | is only used to render a view if a template can be found – in this case, at
432 | `app/views/posts/show.html.curly`.
433 |
434 | Presenters can declare a list of accepted variables using the `presents` method:
435 |
436 | ```ruby
437 | class Posts::ShowPresenter < Curly::Presenter
438 | presents :post
439 | end
440 | ```
441 |
442 | A variable can have a default value:
443 |
444 | ```ruby
445 | class Posts::ShowPresenter < Curly::Presenter
446 | presents :post
447 | presents :comment, default: nil
448 | end
449 | ```
450 |
451 | Any public method defined on the presenter is made available to the template as
452 | a component:
453 |
454 | ```ruby
455 | class Posts::ShowPresenter < Curly::Presenter
456 | presents :post
457 |
458 | def title
459 | @post.title
460 | end
461 |
462 | def author_link
463 | # You can call any Rails helper from within a presenter instance:
464 | link_to author.name, profile_path(author), rel: "author"
465 | end
466 |
467 | private
468 |
469 | # Private methods are not available to the template, so they're safe to
470 | # use.
471 | def author
472 | @post.author
473 | end
474 | end
475 | ```
476 |
477 | Presenter methods can even take an argument. Say your Curly template has the content
478 | `{{t.welcome_message}}`, where `welcome_message` is an I18n key. The following presenter
479 | method would make the lookup work:
480 |
481 | ```ruby
482 | def t(key)
483 | translate(key)
484 | end
485 | ```
486 |
487 | That way, simple ``functions'' can be added to the Curly language. Make sure these do not
488 | have any side effects, though, as an important part of Curly is the idempotence of the
489 | templates.
490 |
491 |
492 | ### Layouts and content blocks
493 |
494 | Both layouts and content blocks (see [`content_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for))
495 | use `yield` to signal that content can be inserted. Curly works just like ERB, so calling
496 | `yield` with no arguments will make the view usable as a layout, while passing a Symbol
497 | will make it try to read a content block with the given name:
498 |
499 | ```ruby
500 | # Given you have the following Curly template in
501 | # app/views/layouts/application.html.curly
502 | #
503 | #
504 | #
505 | #
{{title}}
506 | #
507 | #
508 | #
509 | # {{body}}
510 | #
511 | #
512 | #
513 | class ApplicationLayout < Curly::Presenter
514 | def title
515 | "You can use methods just like in any other presenter!"
516 | end
517 |
518 | def sidebar
519 | # A view can call `content_for(:sidebar) { "some HTML here" }`
520 | yield :sidebar
521 | end
522 |
523 | def body
524 | # The view will be rendered and inserted here:
525 | yield
526 | end
527 | end
528 | ```
529 |
530 |
531 | ### Rails helper methods
532 |
533 | In order to make a Rails helper method available as a component in your template,
534 | use the `exposes_helper` method:
535 |
536 | ```ruby
537 | class Layouts::ApplicationPresenter < Curly::Presenter
538 | # The components {{sign_in_path}} and {{root_path}} are made available.
539 | exposes_helper :sign_in_path, :root_path
540 | end
541 | ```
542 |
543 |
544 | ### Testing
545 |
546 | Presenters can be tested directly, but sometimes it makes sense to integrate with
547 | Rails on some levels. Currently, only RSpec is directly supported, but you can
548 | easily instantiate a presenter:
549 |
550 | ```ruby
551 | SomePresenter.new(context, assigns)
552 | ```
553 |
554 | `context` is a view context, i.e. an object that responds to `render`, has all
555 | the helper methods you expect, etc. You can pass in a test double and see what
556 | you need to stub out. `assigns` is the hash containing the controller and local
557 | assigns. You need to pass in a key for each argument the presenter expects.
558 |
559 | #### Testing with RSpec
560 |
561 | In order to test presenters with RSpec, make sure you have `rspec-rails` in your
562 | Gemfile. Given the following presenter:
563 |
564 | ```ruby
565 | # app/presenters/posts/show_presenter.rb
566 | class Posts::ShowPresenter < Curly::Presenter
567 | presents :post
568 |
569 | def body
570 | Markdown.render(@post.body)
571 | end
572 | end
573 | ```
574 |
575 | You can test the presenter methods like this:
576 |
577 | ```ruby
578 | # You can put this in your `spec_helper.rb`.
579 | require 'curly/rspec'
580 |
581 | # spec/presenters/posts/show_presenter_spec.rb
582 | describe Posts::ShowPresenter, type: :presenter do
583 | describe "#body" do
584 | it "renders the post's body as Markdown" do
585 | assign(:post, double(:post, body: "**hello!**"))
586 | expect(presenter.body).to eq "
hello!"
587 | end
588 | end
589 | end
590 | ```
591 |
592 | Note that your spec *must* be tagged with `type: :presenter`.
593 |
594 |
595 | ### Examples
596 |
597 | Here is a simple Curly template – it will be looked up by Rails automatically.
598 |
599 | ```html
600 |
601 |
{{title}}
602 |
{{author}}
603 |
{{description}}
604 |
605 | {{comment_form}}
606 |
607 |
610 | ```
611 |
612 | When rendering the template, a presenter is automatically instantiated with the
613 | variables assigned in the controller or the `render` call. The presenter declares
614 | the variables it expects with `presents`, which takes a list of variables names.
615 |
616 | ```ruby
617 | # app/presenters/posts/show_presenter.rb
618 | class Posts::ShowPresenter < Curly::Presenter
619 | presents :post
620 |
621 | def title
622 | @post.title
623 | end
624 |
625 | def author
626 | link_to(@post.author.name, @post.author, rel: "author")
627 | end
628 |
629 | def description
630 | Markdown.new(@post.description).to_html.html_safe
631 | end
632 |
633 | def comments
634 | render 'comment', collection: @post.comments
635 | end
636 |
637 | def comment_form
638 | if @post.comments_allowed?
639 | render 'comment_form', post: @post
640 | else
641 | content_tag(:p, "Comments are disabled for this post")
642 | end
643 | end
644 | end
645 | ```
646 |
647 |
648 | Caching
649 | -------
650 |
651 | Caching is handled at two levels in Curly – statically and dynamically. Static caching
652 | concerns changes to your code and templates introduced by deploys. If you do not wish
653 | to clear your entire cache every time you deploy, you need a way to indicate that some
654 | view, helper, or other piece of logic has changed.
655 |
656 | Dynamic caching concerns changes that happen on the fly, usually made by your users in
657 | the running system. You wish to cache a view or a partial and have it expire whenever
658 | some data is updated – usually whenever a specific record is changed.
659 |
660 |
661 | ### Dynamic Caching
662 |
663 | Because of the way logic is contained in presenters, caching entire views or partials
664 | by the data they present becomes exceedingly straightforward. Simply define a
665 | `#cache_key` method that returns a non-nil object, and the return value will be used to
666 | cache the template.
667 |
668 | Whereas in ERB you would include the `cache` call in the template itself:
669 |
670 | ```erb
671 | <% cache([@post, signed_in?]) do %>
672 | ...
673 | <% end %>
674 | ```
675 |
676 | In Curly you would instead declare it in the presenter:
677 |
678 | ```ruby
679 | class Posts::ShowPresenter < Curly::Presenter
680 | presents :post
681 |
682 | def cache_key
683 | [@post, signed_in?]
684 | end
685 | end
686 | ```
687 |
688 | Likewise, you can add a `#cache_duration` method if you wish to automatically expire
689 | the fragment cache:
690 |
691 | ```ruby
692 | class Posts::ShowPresenter < Curly::Presenter
693 | ...
694 |
695 | def cache_duration
696 | 30.minutes
697 | end
698 | end
699 | ```
700 |
701 | In order to set *any* cache option, define a `#cache_options` method that
702 | returns a Hash of options:
703 |
704 | ```ruby
705 | class Posts::ShowPresenter < Curly::Presenter
706 | ...
707 |
708 | def cache_options
709 | { compress: true, namespace: "my-app" }
710 | end
711 | end
712 | ```
713 |
714 |
715 | ### Static Caching
716 |
717 | Static caching will only be enabled for presenters that define a non-nil `#cache_key`
718 | method (see [Dynamic Caching.](#dynamic-caching))
719 |
720 | In order to make a deploy expire the cache for a specific view, set the `version` of the
721 | view to something new, usually by incrementing by one:
722 |
723 | ```ruby
724 | class Posts::ShowPresenter < Curly::Presenter
725 | version 3
726 |
727 | def cache_key
728 | # Some objects
729 | end
730 | end
731 | ```
732 |
733 | This will change the cache keys for all instances of that view, effectively expiring
734 | the old cache entries.
735 |
736 | This works well for views, or for partials that are rendered in views that themselves
737 | are not cached. If the partial is nested within a view that _is_ cached, however, the
738 | outer cache will not be expired. The solution is to register that the inner partial
739 | is a dependency of the outer one such that Curly can automatically deduce that the
740 | outer partial cache should be expired:
741 |
742 | ```ruby
743 | class Posts::ShowPresenter < Curly::Presenter
744 | version 3
745 | depends_on 'posts/comment'
746 |
747 | def cache_key
748 | # Some objects
749 | end
750 | end
751 |
752 | class Posts::CommentPresenter < Curly::Presenter
753 | version 4
754 |
755 | def cache_key
756 | # Some objects
757 | end
758 | end
759 | ```
760 |
761 | Now, if the `version` of `Posts::CommentPresenter` is bumped, the cache keys for both
762 | presenters would change. You can register any number of view paths with `depends_on`.
763 |
764 | Curly integrates well with the
765 | [caching mechanism](http://guides.rubyonrails.org/caching_with_rails.html) in Rails 4 (or
766 | [Cache Digests](https://github.com/rails/cache_digests) in Rails 3), so the dependencies
767 | defined with `depends_on` will be tracked by Rails. This will allow you to deploy changes
768 | to your templates and have the relevant caches automatically expire.
769 |
770 |
771 | Thanks
772 | ------
773 |
774 | Thanks to [Zendesk](http://zendesk.com/) for sponsoring the work on Curly.
775 |
776 |
777 | ### Contributors
778 |
779 | - Daniel Schierbeck ([@dasch](https://github.com/dasch))
780 | - Benjamin Quorning ([@bquorning](https://github.com/bquorning))
781 | - Jeremy Rodi ([@medcat](https://github.com/medcat))
782 | - Alisson Cavalcante Agiani ([@thelinuxlich](https://github.com/thelinuxlich))
783 | - Łukasz Niemier ([@hauleth](https://github.com/hauleth))
784 | - Cristian Planas ([@Gawyn](https://github.com/Gawyn))
785 | - Steven Davidovitz ([@steved](https://github.com/steved))
786 |
787 |
788 | Build Status
789 | ------------
790 |
791 | [](https://github.com/zendesk/curly/actions?query=workflow%3ACI)
792 |
793 | Copyright and License
794 | ---------------------
795 |
796 | Copyright (c) 2013 Daniel Schierbeck (@dasch), Zendesk Inc.
797 |
798 | Licensed under the [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
799 |
--------------------------------------------------------------------------------
63 | {{author_link}} posted {{time_ago}} ago. 64 |
65 | 66 | {{body}} 67 | 68 | {{#author?}} 69 |{{deletion_link}}
70 | {{/author?}} 71 |