curly-templates 2.0.1 → 2.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +2 -0
  4. data/README.md +85 -5
  5. data/curly-templates.gemspec +37 -8
  6. data/lib/curly.rb +1 -1
  7. data/lib/curly/{attribute_parser.rb → attribute_scanner.rb} +6 -4
  8. data/lib/curly/compiler.rb +81 -72
  9. data/lib/curly/component_compiler.rb +37 -31
  10. data/lib/curly/component_scanner.rb +19 -0
  11. data/lib/curly/incomplete_block_error.rb +0 -7
  12. data/lib/curly/incorrect_ending_error.rb +0 -21
  13. data/lib/curly/parser.rb +171 -0
  14. data/lib/curly/presenter.rb +1 -1
  15. data/lib/curly/scanner.rb +23 -9
  16. data/spec/attribute_scanner_spec.rb +46 -0
  17. data/spec/collection_blocks_spec.rb +88 -0
  18. data/spec/compiler/context_blocks_spec.rb +42 -0
  19. data/spec/component_compiler_spec.rb +26 -77
  20. data/spec/component_scanner_spec.rb +19 -0
  21. data/spec/{integration/components_spec.rb → components_spec.rb} +0 -0
  22. data/spec/{integration/conditional_blocks_spec.rb → conditional_blocks_spec.rb} +0 -0
  23. data/spec/dummy/.gitignore +1 -0
  24. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  25. data/spec/dummy/app/controllers/dashboards_controller.rb +13 -0
  26. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  27. data/spec/dummy/app/presenters/dashboards/collection_presenter.rb +7 -0
  28. data/spec/dummy/app/presenters/dashboards/item_presenter.rb +7 -0
  29. data/spec/dummy/app/presenters/dashboards/new_presenter.rb +19 -0
  30. data/spec/dummy/app/presenters/dashboards/partials_presenter.rb +5 -0
  31. data/spec/dummy/app/presenters/dashboards/show_presenter.rb +12 -0
  32. data/spec/dummy/app/presenters/layouts/application_presenter.rb +9 -0
  33. data/spec/dummy/app/views/dashboards/_item.html.curly +1 -0
  34. data/spec/dummy/app/views/dashboards/collection.html.curly +5 -0
  35. data/spec/dummy/app/views/dashboards/new.html.curly +3 -0
  36. data/spec/dummy/app/views/dashboards/partials.html.curly +3 -0
  37. data/spec/dummy/app/views/dashboards/show.html.curly +3 -0
  38. data/spec/dummy/app/views/layouts/application.html.curly +8 -0
  39. data/spec/dummy/config.ru +4 -0
  40. data/spec/dummy/config/application.rb +12 -0
  41. data/spec/dummy/config/boot.rb +5 -0
  42. data/spec/dummy/config/environment.rb +5 -0
  43. data/spec/dummy/config/environments/test.rb +36 -0
  44. data/spec/dummy/config/routes.rb +6 -0
  45. data/spec/integration/application_layout_spec.rb +21 -0
  46. data/spec/integration/collection_blocks_spec.rb +17 -78
  47. data/spec/integration/context_blocks_spec.rb +21 -0
  48. data/spec/integration/partials_spec.rb +23 -0
  49. data/spec/parser_spec.rb +95 -0
  50. data/spec/scanner_spec.rb +24 -14
  51. data/spec/spec_helper.rb +4 -3
  52. metadata +49 -14
  53. data/lib/curly/component_parser.rb +0 -13
  54. data/spec/attribute_parser_spec.rb +0 -46
  55. data/spec/incorrect_ending_error_spec.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e826dc5149782e2edd2cb773a52640b9e57c65eb
4
- data.tar.gz: 1c3672b608b4954d81e1385070c850a22a95d8f7
3
+ metadata.gz: 6cfdffc4b7505394d372817977a7ac58c3eae42d
4
+ data.tar.gz: 55c4eb68a070f354e165c26895fa8168b9d2e28b
5
5
  SHA512:
6
- metadata.gz: bbc93550fccfd6bb3767f2e908db58a9b8386adfb21f0b53d2438fde2d1edb4d6d7034abb6514b38af4158718bdef4f25cb6f8dfb25d044ebb7f533d78c7eae1
7
- data.tar.gz: 87694f0ffd409ca5a60bdc1411234ee7d0d1aec9b596cd45efe1ee6c830a52dbbefdd5dd1c4c073a4d0f3a9302d7d87a420bd0fbacfb7724363b8413bc8f7430
6
+ metadata.gz: 755ce7ead9b1a2330e7416fb770c343bca17b877fcf582d5d38634ff2a83f7c87f6acfef8ad7bfada134d29a4eb49c4411d058d502c0ef2530325670ff9982b0
7
+ data.tar.gz: d947e0a11699021badcd38e870a9bb2b59ae0364011e61b87623db3f747c606f1e898052bc51eec6a5fe1129e3b6196db370abf26330dd689ac06516f456b343
@@ -1,5 +1,11 @@
1
1
  ### Unreleased
2
2
 
3
+ ### Curly 2.1.0
4
+
5
+ * Add support for [context blocks](https://github.com/zendesk/curly#context-blocks).
6
+
7
+ *Daniel Schierbeck*
8
+
3
9
  ### Curly 2.0.1 (September 9, 2014)
4
10
 
5
11
  * Fixed an issue when using Curly with Rails 4.1.
data/Gemfile CHANGED
@@ -8,4 +8,6 @@ platform :ruby do
8
8
  gem 'redcarpet'
9
9
  gem 'github-markup'
10
10
  gem 'coveralls', require: false
11
+ gem 'rails', '~> 4.1.0', require: false
12
+ gem 'rspec-rails', require: false
11
13
  end
data/README.md CHANGED
@@ -28,6 +28,7 @@ or [Handlebars](http://handlebarsjs.com/), Curly is different in some key ways:
28
28
  1. [Attributes](#attributes)
29
29
  1. [Conditional blocks](#conditional-blocks)
30
30
  1. [Collection blocks](#collection-blocks)
31
+ 1. [Context blocks](#context-blocks)
31
32
  1. [Setting up state](#setting-up-state)
32
33
  2. [Escaping Curly syntax](#escaping-curly-syntax)
33
34
  2. [Comments](#comments)
@@ -263,6 +264,84 @@ Collection blocks are an alternative to splitting out a separate template and re
263
264
  that from the presenter – which solution is best depends on your use case.
264
265
 
265
266
 
267
+ ### Context blocks
268
+
269
+ While collection blocks allow you to define the template that should be used to render
270
+ items in a collection right within the parent template, **context blocks** allow you
271
+ to define the template for an arbitrary context. This is very powerful, and can be used
272
+ to define widget-style components and helpers, and provide an easy way to work with
273
+ structured data. Let's say you have a comment form on your page, and you'd rather keep
274
+ the template inline. A simple template could look like:
275
+
276
+ ```html
277
+ <!-- post.html.curly -->
278
+ <h1>{{title}}</h1>
279
+ {{body}}
280
+
281
+ {{@comment_form}}
282
+ <b>Name: </b> {{name_field}}<br>
283
+ <b>E-mail: </b> {{email_field}}<br>
284
+ {{comment_field}}
285
+
286
+ {{submit_button}}
287
+ {{/comment_form}}
288
+ ```
289
+
290
+ Note that an `@` character is used to denote a context block. Like with
291
+ [collection blocks](#collection-blocks), a separate presenter class is used within the
292
+ block, and a simple convention is used to find it. The name of the context component
293
+ (in this case, `comment_form`) will be camel cased, and the current presenter's namespace
294
+ will be searched:
295
+
296
+ ```ruby
297
+ class PostPresenter < Curly::Presenter
298
+ presents :post
299
+ def title; @post.title; end
300
+ def body; markdown(@post.body); end
301
+
302
+ # A context block method *must* take a block argument. The return value
303
+ # of the method will be used when rendering. Calling the block argument will
304
+ # render the nested template. If you pass a value when calling the block
305
+ # argument it will be passed to the presenter.
306
+ def comment_form(&block)
307
+ form_for(Comment.new, &block)
308
+ end
309
+
310
+ # The presenter name is automatically deduced.
311
+ class CommentFormPresenter < Curly::Presenter
312
+ # The value passed to the block argument will be passed in a parameter named
313
+ # after the component.
314
+ presents :comment_form
315
+
316
+ # Any parameters passed to the parent presenter will be forwarded to this
317
+ # presenter as well.
318
+ presents :post
319
+
320
+ def name_field
321
+ @comment_form.text_field :name
322
+ end
323
+
324
+ # ...
325
+ end
326
+ end
327
+ ```
328
+
329
+ Context blocks were designed to work well with Rails' helper methods such as `form_for`
330
+ and `content_tag`, but you can also work directly with the block. For instance, if you
331
+ want to directly control the value that is passed to the nested presenter, you can call
332
+ the `call` method on the block yourself:
333
+
334
+ ```ruby
335
+ def author(&block)
336
+ content_tag :div, class: "author" do
337
+ # The return value of `call` will be the result of rendering the nested template
338
+ # with the argument. You can post-process the string if you want.
339
+ block.call(@post.author)
340
+ end
341
+ end
342
+ ```
343
+
344
+
266
345
  ### Setting up state
267
346
 
268
347
  Although most code in Curly presenters should be free of side effects, sometimes side
@@ -525,7 +604,7 @@ end
525
604
  Static caching will only be enabled for presenters that define a non-nil `#cache_key`
526
605
  method (see [Dynamic Caching.](#dynamic-caching))
527
606
 
528
- In order to make a deploy expire the cache for a specific view, set the version of the
607
+ In order to make a deploy expire the cache for a specific view, set the `version` of the
529
608
  view to something new, usually by incrementing by one:
530
609
 
531
610
  ```ruby
@@ -559,7 +638,6 @@ end
559
638
 
560
639
  class Posts::CommentPresenter < Curly::Presenter
561
640
  version 4
562
- depends_on 'posts/comment'
563
641
 
564
642
  def cache_key
565
643
  # Some objects
@@ -567,11 +645,13 @@ class Posts::CommentPresenter < Curly::Presenter
567
645
  end
568
646
  ```
569
647
 
570
- Now, if the version of `Posts::CommentPresenter` is bumped, the cache keys for both
648
+ Now, if the `version` of `Posts::CommentPresenter` is bumped, the cache keys for both
571
649
  presenters would change. You can register any number of view paths with `depends_on`.
572
650
 
573
- If you use [Cache Digests](https://github.com/rails/cache_digests), Curly will
574
- automatically provide a list of dependencies. This will allow you to deploy changes
651
+ Curly integrates well with the
652
+ [caching mechanism](http://guides.rubyonrails.org/caching_with_rails.html) in Rails 4 (or
653
+ [Cache Digests](https://github.com/rails/cache_digests) in Rails 3), so the dependencies
654
+ defined with `depends_on` will be tracked by Rails. This will allow you to deploy changes
575
655
  to your templates and have the relevant caches automatically expire.
576
656
 
577
657
 
@@ -4,8 +4,8 @@ Gem::Specification.new do |s|
4
4
  s.rubygems_version = '1.3.5'
5
5
 
6
6
  s.name = 'curly-templates'
7
- s.version = '2.0.1'
8
- s.date = '2014-09-09'
7
+ s.version = '2.1.0.beta1'
8
+ s.date = '2014-11-03'
9
9
 
10
10
  s.summary = "Free your views!"
11
11
  s.description = "A view layer for your Rails apps that separates structure and logic."
@@ -36,15 +36,16 @@ Gem::Specification.new do |s|
36
36
  curly-templates.gemspec
37
37
  lib/curly-templates.rb
38
38
  lib/curly.rb
39
- lib/curly/attribute_parser.rb
39
+ lib/curly/attribute_scanner.rb
40
40
  lib/curly/compiler.rb
41
41
  lib/curly/component_compiler.rb
42
- lib/curly/component_parser.rb
42
+ lib/curly/component_scanner.rb
43
43
  lib/curly/dependency_tracker.rb
44
44
  lib/curly/error.rb
45
45
  lib/curly/incomplete_block_error.rb
46
46
  lib/curly/incorrect_ending_error.rb
47
47
  lib/curly/invalid_component.rb
48
+ lib/curly/parser.rb
48
49
  lib/curly/presenter.rb
49
50
  lib/curly/presenter_not_found.rb
50
51
  lib/curly/railtie.rb
@@ -55,15 +56,43 @@ Gem::Specification.new do |s|
55
56
  lib/generators/curly/controller/templates/presenter.rb.erb
56
57
  lib/generators/curly/controller/templates/view.html.curly.erb
57
58
  lib/rails/projections.json
58
- spec/attribute_parser_spec.rb
59
+ spec/attribute_scanner_spec.rb
60
+ spec/collection_blocks_spec.rb
59
61
  spec/compiler/collections_spec.rb
62
+ spec/compiler/context_blocks_spec.rb
60
63
  spec/compiler_spec.rb
61
64
  spec/component_compiler_spec.rb
65
+ spec/component_scanner_spec.rb
66
+ spec/components_spec.rb
67
+ spec/conditional_blocks_spec.rb
68
+ spec/dummy/.gitignore
69
+ spec/dummy/app/controllers/application_controller.rb
70
+ spec/dummy/app/controllers/dashboards_controller.rb
71
+ spec/dummy/app/helpers/application_helper.rb
72
+ spec/dummy/app/presenters/dashboards/collection_presenter.rb
73
+ spec/dummy/app/presenters/dashboards/item_presenter.rb
74
+ spec/dummy/app/presenters/dashboards/new_presenter.rb
75
+ spec/dummy/app/presenters/dashboards/partials_presenter.rb
76
+ spec/dummy/app/presenters/dashboards/show_presenter.rb
77
+ spec/dummy/app/presenters/layouts/application_presenter.rb
78
+ spec/dummy/app/views/dashboards/_item.html.curly
79
+ spec/dummy/app/views/dashboards/collection.html.curly
80
+ spec/dummy/app/views/dashboards/new.html.curly
81
+ spec/dummy/app/views/dashboards/partials.html.curly
82
+ spec/dummy/app/views/dashboards/show.html.curly
83
+ spec/dummy/app/views/layouts/application.html.curly
84
+ spec/dummy/config.ru
85
+ spec/dummy/config/application.rb
86
+ spec/dummy/config/boot.rb
87
+ spec/dummy/config/environment.rb
88
+ spec/dummy/config/environments/test.rb
89
+ spec/dummy/config/routes.rb
62
90
  spec/generators/controller_generator_spec.rb
63
- spec/incorrect_ending_error_spec.rb
91
+ spec/integration/application_layout_spec.rb
64
92
  spec/integration/collection_blocks_spec.rb
65
- spec/integration/components_spec.rb
66
- spec/integration/conditional_blocks_spec.rb
93
+ spec/integration/context_blocks_spec.rb
94
+ spec/integration/partials_spec.rb
95
+ spec/parser_spec.rb
67
96
  spec/presenter_spec.rb
68
97
  spec/scanner_spec.rb
69
98
  spec/spec_helper.rb
@@ -26,7 +26,7 @@
26
26
  # See Curly::Presenter for more information on presenters.
27
27
  #
28
28
  module Curly
29
- VERSION = "2.0.1"
29
+ VERSION = "2.1.0.beta1"
30
30
 
31
31
  # Compiles a Curly template to Ruby code.
32
32
  #
@@ -1,17 +1,19 @@
1
+ require 'curly/error'
2
+
1
3
  module Curly
2
4
  AttributeError = Class.new(Curly::Error)
3
5
 
4
- class AttributeParser
5
- def self.parse(string)
6
+ class AttributeScanner
7
+ def self.scan(string)
6
8
  return {} if string.nil?
7
- new(string).parse
9
+ new(string).scan
8
10
  end
9
11
 
10
12
  def initialize(string)
11
13
  @scanner = StringScanner.new(string)
12
14
  end
13
15
 
14
- def parse
16
+ def scan
15
17
  attributes = scan_attributes
16
18
  Hash[attributes]
17
19
  end
@@ -1,10 +1,8 @@
1
1
  require 'curly/scanner'
2
+ require 'curly/parser'
2
3
  require 'curly/component_compiler'
3
- require 'curly/component_parser'
4
4
  require 'curly/error'
5
5
  require 'curly/invalid_component'
6
- require 'curly/incorrect_ending_error'
7
- require 'curly/incomplete_block_error'
8
6
 
9
7
  module Curly
10
8
 
@@ -26,7 +24,16 @@ module Curly
26
24
  # Raises IncompleteBlockError if a block is not completed.
27
25
  # Returns a String containing the Ruby code.
28
26
  def self.compile(template, presenter_class)
29
- new(template, presenter_class).compile
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
30
37
  end
31
38
 
32
39
  # Whether the Curly template is valid. This includes whether all
@@ -44,34 +51,22 @@ module Curly
44
51
  false
45
52
  end
46
53
 
47
- attr_reader :template
48
-
49
- def initialize(template, presenter_class)
50
- @template = template
54
+ def initialize(presenter_class)
51
55
  @presenter_classes = [presenter_class]
56
+ @parts = []
52
57
  end
53
58
 
54
- def compile
55
- if presenter_class.nil?
56
- raise ArgumentError, "presenter class cannot be nil"
57
- end
58
-
59
- tokens = Scanner.scan(template)
60
-
61
- @blocks = []
62
-
63
- parts = tokens.map do |type, value|
64
- send("compile_#{type}", value)
65
- end
66
-
67
- if @blocks.any?
68
- raise IncompleteBlockError.new(@blocks.pop)
59
+ def compile(nodes)
60
+ nodes.each do |node|
61
+ send("compile_#{node.type}", node)
69
62
  end
63
+ end
70
64
 
65
+ def code
71
66
  <<-RUBY
72
67
  buffer = ActiveSupport::SafeBuffer.new
73
68
  presenters = []
74
- #{parts.join("\n")}
69
+ #{@parts.join("\n")}
75
70
  buffer
76
71
  RUBY
77
72
  end
@@ -82,100 +77,114 @@ module Curly
82
77
  @presenter_classes.last
83
78
  end
84
79
 
85
- def compile_conditional_block_start(component)
86
- compile_conditional_block "if", component
80
+ def compile_conditional(block)
81
+ compile_conditional_block("if", block)
87
82
  end
88
83
 
89
- def compile_inverse_conditional_block_start(component)
90
- compile_conditional_block "unless", component
84
+ def compile_inverse_conditional(block)
85
+ compile_conditional_block("unless", block)
91
86
  end
92
87
 
93
- def compile_collection_block_start(component)
94
- name, identifier, attributes = ComponentParser.parse(component)
95
- method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
88
+ def compile_collection(block)
89
+ component = block.component
90
+ method_call = ComponentCompiler.compile(presenter_class, component)
96
91
 
97
- as = name.singularize
98
- counter = "#{as}_counter"
92
+ name = component.name.singularize
93
+ counter = "#{name}_counter"
99
94
 
100
95
  begin
101
- item_presenter_class = presenter_class.presenter_for_name(as)
96
+ item_presenter_class = presenter_class.presenter_for_name(name)
102
97
  rescue NameError
103
98
  raise Curly::Error,
104
- "cannot enumerate `#{component}`, could not find matching presenter class"
99
+ "cannot enumerate `#{name}`, could not find matching presenter class"
105
100
  end
106
101
 
107
- push_block(name, identifier)
108
- @presenter_classes.push(item_presenter_class)
109
-
110
- <<-RUBY
102
+ output <<-RUBY
111
103
  presenters << presenter
112
104
  items = Array(#{method_call})
113
105
  items.each_with_index do |item, index|
114
- item_options = options.merge(:#{as} => item, :#{counter} => index + 1)
106
+ item_options = options.merge(:#{name} => item, :#{counter} => index + 1)
115
107
  presenter = #{item_presenter_class}.new(self, item_options)
116
108
  RUBY
109
+
110
+ @presenter_classes.push(item_presenter_class)
111
+ compile(block.nodes)
112
+ @presenter_classes.pop
113
+
114
+ output <<-RUBY
115
+ end
116
+ presenter = presenters.pop
117
+ RUBY
117
118
  end
118
119
 
119
- def compile_conditional_block(keyword, component)
120
- name, identifier, attributes = ComponentParser.parse(component)
121
- method_call = ComponentCompiler.compile_conditional(presenter_class, name, identifier, attributes)
120
+ def compile_conditional_block(keyword, block)
121
+ component = block.component
122
+ method_call = ComponentCompiler.compile(presenter_class, component)
122
123
 
123
- push_block(name, identifier)
124
+ unless component.name.end_with?("?")
125
+ raise Curly::Error, "conditional components must end with `?`"
126
+ end
124
127
 
125
- <<-RUBY
128
+ output <<-RUBY
126
129
  #{keyword} #{method_call}
127
130
  RUBY
128
- end
129
131
 
130
- def compile_conditional_block_end(component)
131
- validate_block_end(component)
132
+ compile(block.nodes)
132
133
 
133
- <<-RUBY
134
+ output <<-RUBY
134
135
  end
135
136
  RUBY
136
137
  end
137
138
 
138
- def compile_collection_block_end(component)
139
+ def compile_context(block)
140
+ component = block.component
141
+ method_call = ComponentCompiler.compile(presenter_class, component, type: block.type)
142
+
143
+ name = component.name
144
+
145
+ begin
146
+ item_presenter_class = presenter_class.presenter_for_name(name)
147
+ rescue NameError
148
+ raise Curly::Error,
149
+ "cannot use context `#{name}`, could not find matching presenter class"
150
+ end
151
+
152
+ output <<-RUBY
153
+ presenters << presenter
154
+ old_buffer, buffer = buffer, ActiveSupport::SafeBuffer.new
155
+ old_buffer << #{method_call} do |item|
156
+ item_options = options.merge(:#{name} => item)
157
+ presenter = #{item_presenter_class}.new(self, item_options.with_indifferent_access)
158
+ RUBY
159
+
160
+ @presenter_classes.push(item_presenter_class)
161
+ compile(block.nodes)
139
162
  @presenter_classes.pop
140
- validate_block_end(component)
141
163
 
142
- <<-RUBY
164
+ output <<-RUBY
143
165
  end
166
+ buffer = old_buffer
144
167
  presenter = presenters.pop
145
168
  RUBY
146
169
  end
147
170
 
148
171
  def compile_component(component)
149
- name, identifier, attributes = ComponentParser.parse(component)
150
- method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
172
+ method_call = ComponentCompiler.compile(presenter_class, component)
151
173
  code = "#{method_call} {|*args| yield(*args) }"
152
174
 
153
- "buffer.concat(#{code.strip}.to_s)"
175
+ output "buffer.concat(#{code.strip}.to_s)"
154
176
  end
155
177
 
156
178
  def compile_text(text)
157
- "buffer.safe_concat(#{text.inspect})"
179
+ output "buffer.safe_concat(#{text.value.inspect})"
158
180
  end
159
181
 
160
182
  def compile_comment(comment)
161
- "" # Replace the content with an empty string.
162
- end
163
-
164
- def validate_block_end(component)
165
- name, identifier, attributes = ComponentParser.parse(component)
166
- last_block = @blocks.pop
167
-
168
- if last_block.nil?
169
- raise Curly::Error, "block ending not expected"
170
- end
171
-
172
- unless last_block == [name, identifier]
173
- raise Curly::IncorrectEndingError.new([name, identifier], last_block)
174
- end
183
+ # Do nothing.
175
184
  end
176
185
 
177
- def push_block(name, identifier)
178
- @blocks.push([name, identifier])
186
+ def output(code)
187
+ @parts << code
179
188
  end
180
189
  end
181
190
  end