curly-templates 2.0.1 → 2.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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