lotus-view 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -15
  3. data/.travis.yml +6 -0
  4. data/.yardopts +3 -0
  5. data/Gemfile +13 -2
  6. data/README.md +514 -3
  7. data/Rakefile +17 -1
  8. data/lib/lotus/layout.rb +132 -0
  9. data/lib/lotus/presenter.rb +70 -0
  10. data/lib/lotus/view/dsl.rb +247 -0
  11. data/lib/lotus/view/inheritable.rb +50 -0
  12. data/lib/lotus/view/rendering/layout_finder.rb +104 -0
  13. data/lib/lotus/view/rendering/layout_registry.rb +63 -0
  14. data/lib/lotus/view/rendering/layout_scope.rb +138 -0
  15. data/lib/lotus/view/rendering/null_layout.rb +52 -0
  16. data/lib/lotus/view/rendering/null_template.rb +79 -0
  17. data/lib/lotus/view/rendering/partial.rb +29 -0
  18. data/lib/lotus/view/rendering/partial_finder.rb +41 -0
  19. data/lib/lotus/view/rendering/registry.rb +129 -0
  20. data/lib/lotus/view/rendering/scope.rb +48 -0
  21. data/lib/lotus/view/rendering/template.rb +56 -0
  22. data/lib/lotus/view/rendering/template_finder.rb +53 -0
  23. data/lib/lotus/view/rendering/templates_finder.rb +85 -0
  24. data/lib/lotus/view/rendering/view_finder.rb +37 -0
  25. data/lib/lotus/view/rendering.rb +265 -0
  26. data/lib/lotus/view/template.rb +45 -0
  27. data/lib/lotus/view/version.rb +4 -1
  28. data/lib/lotus/view.rb +180 -2
  29. data/lib/lotus-view.rb +1 -0
  30. data/lotus-view.gemspec +15 -11
  31. data/test/fixtures/templates/app/app_view.html.erb +0 -0
  32. data/test/fixtures/templates/app/view.html.erb +0 -0
  33. data/test/fixtures/templates/application.html.erb +10 -0
  34. data/test/fixtures/templates/articles/_form.html.erb +4 -0
  35. data/test/fixtures/templates/articles/alternative_new.html.erb +1 -0
  36. data/test/fixtures/templates/articles/index.atom.erb +5 -0
  37. data/test/fixtures/templates/articles/index.html.erb +3 -0
  38. data/test/fixtures/templates/articles/index.json.erb +9 -0
  39. data/test/fixtures/templates/articles/index.rss.erb +0 -0
  40. data/test/fixtures/templates/articles/new.html.erb +7 -0
  41. data/test/fixtures/templates/articles/show.html.erb +1 -0
  42. data/test/fixtures/templates/articles/show.json.erb +5 -0
  43. data/test/fixtures/templates/contacts/show.html.haml +1 -0
  44. data/test/fixtures/templates/dashboard/index.html.erb +2 -0
  45. data/test/fixtures/templates/hello_world_view.html.erb +1 -0
  46. data/test/fixtures/templates/index_view.html.erb +1 -0
  47. data/test/fixtures/templates/json_render_view.json.erb +3 -0
  48. data/test/fixtures/templates/render_view.html.erb +1 -0
  49. data/test/fixtures/templates/shared/_sidebar.html.erb +1 -0
  50. data/test/fixtures.rb +187 -0
  51. data/test/layout_test.rb +10 -0
  52. data/test/load_test.rb +79 -0
  53. data/test/presenter_test.rb +31 -0
  54. data/test/rendering_test.rb +125 -0
  55. data/test/root_test.rb +38 -0
  56. data/test/test_helper.rb +24 -0
  57. data/test/version_test.rb +7 -0
  58. data/test/view_test.rb +27 -0
  59. metadata +137 -10
@@ -0,0 +1,85 @@
1
+ require 'lotus/view/template'
2
+
3
+ module Lotus
4
+ module View
5
+ module Rendering
6
+ # Find templates for a view
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ #
11
+ # @see View::Template
12
+ class TemplatesFinder
13
+ FORMAT = '*'.freeze
14
+ ENGINES = '*'.freeze
15
+
16
+ # Initialize a finder
17
+ #
18
+ # @param view [Class] the view
19
+ #
20
+ # @api private
21
+ # @since 0.1.0
22
+ def initialize(view)
23
+ @view = view
24
+ end
25
+
26
+ # Find all the associated templates to the view.
27
+ # It looks for templates under the root path of the view, that are
28
+ # matching the template name
29
+ #
30
+ # @return [Array<Lotus::View::Template>] the templates
31
+ #
32
+ # @api private
33
+ # @since 0.1.0
34
+ #
35
+ # @see Lotus::View::Dsl#root
36
+ # @see Lotus::View::Dsl#template
37
+ #
38
+ # @example
39
+ # require 'lotus/view'
40
+ #
41
+ # module Articles
42
+ # class Show
43
+ # include Lotus::View
44
+ # end
45
+ # end
46
+ #
47
+ # Articles::Show.root # => "/path/to/templates"
48
+ # Articles::Show.template # => "articles/show"
49
+ #
50
+ # # This view has a template:
51
+ # #
52
+ # # "/path/to/templates/articles/show.html.erb"
53
+ #
54
+ # Lotus::View::Rendering::TemplatesFinder.new(Articles::Show).find
55
+ # # => [#<Lotus::View::Template:0x007f8a0a86a970 ... @file="/path/to/templates/articles/show.html.erb">]
56
+ def find
57
+ Dir.glob( "#{ [root, template_name].join(separator) }.#{ format }.#{ engines }" ).map do |template|
58
+ View::Template.new template
59
+ end
60
+ end
61
+
62
+ protected
63
+ def template_name
64
+ @view.template
65
+ end
66
+
67
+ def root
68
+ @view.root
69
+ end
70
+
71
+ def separator
72
+ ::File::SEPARATOR
73
+ end
74
+
75
+ def format
76
+ FORMAT
77
+ end
78
+
79
+ def engines
80
+ ENGINES
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,37 @@
1
+ module Lotus
2
+ module View
3
+ module Rendering
4
+ # Find a view
5
+ #
6
+ # @api private
7
+ # @since 0.1.0
8
+ #
9
+ # @see Lotus::View::Rendering::Registry
10
+ class ViewFinder
11
+ # Initialize a finder
12
+ #
13
+ # @param view [Class] the superclass view
14
+ #
15
+ # @api private
16
+ # @since 0.1.0
17
+ def initialize(view)
18
+ @view = view
19
+ end
20
+
21
+ # Find a view for the given template.
22
+ # It looks up for the current view and its subclasses.
23
+ #
24
+ # @param template [Lotus::View::Template] a template to be associated
25
+ # to a view
26
+ #
27
+ # @return [Class] a view associated with the given template
28
+ #
29
+ # @api private
30
+ # @since 0.1.0
31
+ def find(template)
32
+ @view.subclasses.find {|v| v.format == template.format } || @view
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,265 @@
1
+ require 'lotus/view/rendering/registry'
2
+ require 'lotus/view/rendering/scope'
3
+
4
+ module Lotus
5
+ module View
6
+ # Rendering methods
7
+ #
8
+ # @since 0.1.0
9
+ #
10
+ # @see Lotus::View::Rendering::InstanceMethods
11
+ module Rendering
12
+ def self.extended(base)
13
+ base.class_eval do
14
+ include InstanceMethods
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ # Initialize a view
20
+ #
21
+ # @param template [Lotus::View::Template] the template to render
22
+ # @param locals [Hash] a set of objects available during the rendering
23
+ # process.
24
+ #
25
+ # @since 0.1.0
26
+ #
27
+ # @see Lotus::View::Template
28
+ #
29
+ # @example
30
+ # require 'lotus/view'
31
+ #
32
+ # class IndexView
33
+ # include Lotus::View
34
+ # end
35
+ #
36
+ # template = Lotus::View::Template.new('index.html.erb')
37
+ # view = IndexView.new(template, {article: article})
38
+ def initialize(template, locals)
39
+ @template = template
40
+ @locals = locals
41
+ @scope = Scope.new(self, @locals)
42
+ end
43
+
44
+ # Render the template by bounding the local scope.
45
+ # If it uses a layout, it renders the template first and then the
46
+ # control passes to the layout.
47
+ #
48
+ # Override this method for custom rendering policies.
49
+ # For instance, when a serializer is used and there isn't the need of
50
+ # a template.
51
+ #
52
+ # @return [String] the output of the rendering process
53
+ #
54
+ # @raise [Lotus::View::MissingTemplateError] if the template is nil
55
+ #
56
+ # @since 0.1.0
57
+ #
58
+ # @see Lotus::View::Layout
59
+ #
60
+ # @example with template
61
+ # require 'lotus/view'
62
+ #
63
+ # class IndexView
64
+ # include Lotus::View
65
+ # end
66
+ #
67
+ # template = Lotus::View::Template.new('index.html.erb')
68
+ # view = IndexView.new(template, {article: article})
69
+ #
70
+ # view.render # => <h1>Introducing Lotus::view</h1> ...
71
+ #
72
+ # @example with template and layout
73
+ # require 'lotus/view'
74
+ #
75
+ # class ApplicationLayout
76
+ # include Lotus::View::Layout
77
+ # end
78
+ #
79
+ # class IndexView
80
+ # include Lotus::View
81
+ # layout :application
82
+ # end
83
+ #
84
+ # template = Lotus::View::Template.new('index.html.erb')
85
+ # view = IndexView.new(template, {article: article})
86
+ #
87
+ # view.render # => <html> ... <h1>Introducing Lotus::view</h1> ...
88
+ #
89
+ # @example with custom rendering
90
+ # require 'lotus/view'
91
+ #
92
+ # class IndexView
93
+ # include Lotus::View
94
+ #
95
+ # def render
96
+ # ArticleSerializer.new(article).render
97
+ # end
98
+ # end
99
+ #
100
+ # view = IndexView.new(nil, {article: article})
101
+ #
102
+ # view.render # => {title: ...}
103
+ def render
104
+ layout.render
105
+ end
106
+
107
+ protected
108
+ # The output of the template rendering process.
109
+ #
110
+ # @return [String] the rendering output
111
+ #
112
+ # @raise [Lotus::View::MissingTemplateError] if the template is nil
113
+ #
114
+ # @api private
115
+ # @since 0.1.0
116
+ def rendered
117
+ template.render @scope
118
+ end
119
+
120
+ # The layout.
121
+ #
122
+ # @return [Class, Lotus::View::Rendering::NullLayout]
123
+ #
124
+ # @see Lotus::View::Layout
125
+ # @see Lotus::View.layout
126
+ # @see Lotus::View::Dsl#layout
127
+ #
128
+ # @api private
129
+ # @since 0.1.0
130
+ def layout
131
+ @layout ||= self.class.layout.new(@scope, rendered)
132
+ end
133
+
134
+ # The template.
135
+ #
136
+ # @return [Lotus::View::Template] the template
137
+ #
138
+ # @raise [Lotus::View::MissingTemplateError] if the template is nil
139
+ #
140
+ # @api private
141
+ # @since 0.1.0
142
+ def template
143
+ @template or raise MissingTemplateError.new(self.class.template, @scope.format)
144
+ end
145
+
146
+ # A set of objects available during the rendering process.
147
+ #
148
+ # @return [Hash]
149
+ #
150
+ # @see Lotus::View#initialize
151
+ #
152
+ # @api private
153
+ # @since 0.1.0
154
+ def locals
155
+ @locals
156
+ end
157
+
158
+ # Delegates missing methods to the scope.
159
+ #
160
+ # @see Lotus::View::Rendering::Scope
161
+ #
162
+ # @api private
163
+ # @since 0.1.0
164
+ #
165
+ # @example
166
+ # require 'lotus/view'
167
+ #
168
+ # class IndexView
169
+ # include Lotus::View
170
+ # end
171
+ #
172
+ # template = Lotus::View::Template.new('index.html.erb')
173
+ # view = IndexView.new(template, {article: article})
174
+ #
175
+ # view.article # => #<Article:0x007fb0bbd3b6e8>
176
+ def method_missing(m)
177
+ @scope.__send__ m
178
+ end
179
+ end
180
+
181
+ # Render the given context and locals with the appropriate template.
182
+ # If there are registered subclasses, it choose the right class, according
183
+ # to the requested format.
184
+ #
185
+ # @param context [Hash] the context for the rendering process
186
+ # @option context [Symbol] :format the requested format
187
+ #
188
+ # @return [String] the output of the rendering process
189
+ #
190
+ # @raise [Lotus::View::MissingTemplateError] if it can't find a template
191
+ # for the given context
192
+ #
193
+ # @raise [Lotus::View::MissingFormatError] if the given context doesn't
194
+ # have the :format key
195
+ #
196
+ # @since 0.1.0
197
+ #
198
+ # @see Lotus::View#initialize
199
+ # @see Lotus::View#render
200
+ #
201
+ # @example
202
+ # require 'lotus/view'
203
+ #
204
+ # article = OpenStruct.new(title: 'Hello')
205
+ #
206
+ # module Articles
207
+ # class Show
208
+ # include Lotus::View
209
+ #
210
+ # def title
211
+ # @title ||= article.title.upcase
212
+ # end
213
+ # end
214
+ #
215
+ # class JsonShow < Show
216
+ # format :json
217
+ #
218
+ # def title
219
+ # super.downcase
220
+ # end
221
+ # end
222
+ # end
223
+ #
224
+ # Lotus::View.root = '/path/to/templates'
225
+ # Lotus::View.load!
226
+ #
227
+ # Articles::Show.render(format: :html, article: article)
228
+ # # => renders `articles/show.html.erb`
229
+ #
230
+ # Articles::Show.render(format: :json, article: article)
231
+ # # => renders `articles/show.json.erb`
232
+ #
233
+ # Articles::Show.render(format: :xml, article: article)
234
+ # # => raises Lotus::View::MissingTemplateError
235
+ def render(context)
236
+ registry.resolve(context).render
237
+ end
238
+
239
+ protected
240
+
241
+ # Loading mechanism hook.
242
+ #
243
+ # @api private
244
+ # @since 0.1.0
245
+ #
246
+ # @see Lotus::View.load!
247
+ def load!
248
+ super
249
+ registry.freeze
250
+ end
251
+
252
+ private
253
+
254
+ # The registry that holds all the registered subclasses.
255
+ #
256
+ # @api private
257
+ # @since 0.1.0
258
+ #
259
+ # @see Lotus::View::Rendering::Registry
260
+ def registry
261
+ @@registry ||= Registry.new(self)
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,45 @@
1
+ require 'tilt'
2
+
3
+ module Lotus
4
+ module View
5
+ # A logic-less template.
6
+ #
7
+ # @since 0.1.0
8
+ class Template
9
+ def initialize(template)
10
+ @_template = Tilt.new(template)
11
+ end
12
+
13
+ # Returns the format that the template handles.
14
+ #
15
+ # @return [String] the format name
16
+ #
17
+ # @since 0.1.0
18
+ #
19
+ # @example
20
+ # require 'lotus/view'
21
+ #
22
+ # template = Lotus::View::Template.new('index.html.erb')
23
+ # template.format # => 'html'
24
+ def format
25
+ @_template.basename.match(/(\.[^.]+)/).to_s.
26
+ gsub('.', ''). # TODO shame on me, this should be part of the regex above
27
+ to_sym
28
+ end
29
+
30
+ # Render the template within the context of the given scope.
31
+ #
32
+ # @param scope [Lotus::View::Scope] the rendering scope
33
+ #
34
+ # @return [String] the output of the rendering process
35
+ #
36
+ # @api private
37
+ # @since 0.1.0
38
+ #
39
+ # @see Lotus::View::Scope
40
+ def render(scope, &blk)
41
+ @_template.render(scope, {}, &blk)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,8 @@
1
1
  module Lotus
2
2
  module View
3
- VERSION = "0.0.0"
3
+ # Defines the version
4
+ #
5
+ # @since 0.1.0
6
+ VERSION = '0.1.0'.freeze
4
7
  end
5
8
  end
data/lib/lotus/view.rb CHANGED
@@ -1,7 +1,185 @@
1
- require "lotus/view/version"
1
+ require 'set'
2
+ require 'pathname'
3
+ require 'lotus/view/version'
4
+ require 'lotus/view/inheritable'
5
+ require 'lotus/view/rendering'
6
+ require 'lotus/view/dsl'
7
+ require 'lotus/layout'
8
+ require 'lotus/presenter'
2
9
 
3
10
  module Lotus
11
+ # View
12
+ #
13
+ # @since 0.1.0
4
14
  module View
5
- # Your code goes here...
15
+ # Missing template error
16
+ #
17
+ # This is raised at the runtime when Lotus::View cannot find a template for
18
+ # the requested format.
19
+ #
20
+ # We can't raise this error during the loading phase, because at that time
21
+ # we don't know if a view implements its own rendering policy.
22
+ # A view is allowed to override `#render`, and this scenario can make the
23
+ # presence of a template useless. One typical example is the usage of a
24
+ # serializer that returns the output string, without rendering a template.
25
+ #
26
+ # @since 0.1.0
27
+ class MissingTemplateError < ::StandardError
28
+ def initialize(template, format)
29
+ super("Can't find template '#{ template }' for '#{ format }' format.")
30
+ end
31
+ end
32
+
33
+ # Missing format error
34
+ #
35
+ # This is raised at the runtime when rendering context lacks of the :format
36
+ # key.
37
+ #
38
+ # @since 0.1.0
39
+ #
40
+ # @see Lotus::View::Rendering#render
41
+ class MissingFormatError < ::StandardError
42
+ end
43
+
44
+ # Register a view
45
+ #
46
+ # @api private
47
+ # @since 0.1.0
48
+ #
49
+ # @example
50
+ # require 'lotus/view'
51
+ #
52
+ # class IndexView
53
+ # include Lotus::View
54
+ # end
55
+ def self.included(base)
56
+ base.class_eval do
57
+ extend Inheritable.dup
58
+ extend Dsl.dup
59
+ extend Rendering.dup
60
+ end
61
+
62
+ views.add(base)
63
+ end
64
+
65
+ # Set the directory root where templates are located
66
+ #
67
+ # @param root [String] the root path
68
+ #
69
+ # @see Lotus::View.root
70
+ #
71
+ # @since 0.1.0
72
+ #
73
+ # @example
74
+ # require 'lotus/view'
75
+ #
76
+ # Lotus::View.root = '/path/to/templates'
77
+ def self.root=(root)
78
+ @root = Pathname.new(root) rescue nil
79
+ end
80
+
81
+ # Returns the directory root where templates are located.
82
+ # If not already set, it returns the current directory.
83
+ #
84
+ # @return [Pathname] the root path for templates
85
+ #
86
+ # @see Lotus::View.root=
87
+ #
88
+ # @since 0.1.0
89
+ #
90
+ # @example with already set value
91
+ # require 'lotus/view'
92
+ #
93
+ # Lotus::View.root = '/path/to/templates'
94
+ # Lotus::View.root # => #<Pathname:/path/to/templates>
95
+ #
96
+ # @example with missing set value
97
+ # require 'lotus/view'
98
+ #
99
+ # Lotus::View.root # => #<Pathname:.>
100
+ def self.root
101
+ @root ||= begin
102
+ self.root = '.'
103
+ @root
104
+ end
105
+ end
106
+
107
+ # Sets the default layout for all the registered views.
108
+ #
109
+ # @param layout [Symbol] the layout name
110
+ #
111
+ # @since 0.1.0
112
+ #
113
+ # @see Lotus::View::Dsl#layout
114
+ # @see Lotus::View.load!
115
+ #
116
+ # @example
117
+ # require 'lotus/view'
118
+ #
119
+ # Lotus::View.layout = :application
120
+ #
121
+ # class IndexView
122
+ # include Lotus::View
123
+ # end
124
+ #
125
+ # Lotus::View.load!
126
+ # IndexView.layout # => ApplicationLayout
127
+ def self.layout=(layout)
128
+ @layout = Rendering::LayoutFinder.find(layout)
129
+ end
130
+
131
+ # Returns the default layout to assign to the registered views.
132
+ # If not already set, it returns a <tt>Rendering::NullLayout</tt>.
133
+ #
134
+ # @return [Class,Rendering::NullLayout] depends if already set or not.
135
+ #
136
+ # @since 0.1.0
137
+ #
138
+ # @see Lotus::View.layout=
139
+ def self.layout
140
+ @layout ||= Rendering::NullLayout
141
+ end
142
+
143
+ # A set of registered views.
144
+ #
145
+ # @return [Set] all the registered views.
146
+ #
147
+ # @api private
148
+ # @since 0.1.0
149
+ def self.views
150
+ @views ||= Set.new
151
+ end
152
+
153
+ # A set of registered layouts.
154
+ #
155
+ # @return [Set] all the registered layout.
156
+ #
157
+ # @api private
158
+ # @since 0.1.0
159
+ def self.layouts
160
+ @layouts ||= Set.new
161
+ end
162
+
163
+ #FIXME extract a Loader class
164
+ def self.load!
165
+ root.freeze
166
+ layout.freeze
167
+ views.freeze
168
+
169
+ views.each do |view|
170
+ view.send(:load!)
171
+ end
172
+
173
+ layouts.each do |layout|
174
+ layout.send(:load!)
175
+ end
176
+ end
177
+
178
+ def self.unload!
179
+ instance_variable_set(:@root, nil)
180
+ instance_variable_set(:@layout, nil)
181
+ instance_variable_set(:@views, Set.new)
182
+ instance_variable_set(:@layouts, Set.new)
183
+ end
6
184
  end
7
185
  end
data/lib/lotus-view.rb ADDED
@@ -0,0 +1 @@
1
+ require 'lotus/view'
data/lotus-view.gemspec CHANGED
@@ -4,20 +4,24 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'lotus/view/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "lotus-view"
7
+ spec.name = 'lotus-view'
8
8
  spec.version = Lotus::View::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
11
- spec.summary = %q{View layer for Lotus}
9
+ spec.authors = ['Luca Guidi']
10
+ spec.email = ['me@lucaguidi.com']
12
11
  spec.description = %q{View layer for Lotus}
13
- spec.homepage = ""
14
- spec.license = "MIT"
12
+ spec.summary = %q{View layer for Lotus, with a separation between views and templates}
13
+ spec.homepage = 'http://lotusrb.org'
14
+ spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
17
+ spec.executables = []
18
+ spec.test_files = spec.files.grep(%r{^(test)/})
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.5"
22
- spec.add_development_dependency "rake"
21
+ spec.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.1'
22
+ spec.add_runtime_dependency 'lotus-utils', '~> 0.1'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.5'
25
+ spec.add_development_dependency 'minitest', '~> 5'
26
+ spec.add_development_dependency 'rake', '~> 10'
23
27
  end
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ <html>
2
+ <head>
3
+ <title><%= title %></title>
4
+ </head>
5
+
6
+ <body>
7
+ <%= render partial: 'shared/sidebar' %>
8
+ <%= yield %>
9
+ </body>
10
+ </html>
@@ -0,0 +1,4 @@
1
+ <form>
2
+ <input type="hidden" name="secret" value="<%= secret %>" />
3
+ <input type="text" name="article[title]" value="<%= article.title %>" />
4
+ </form>
@@ -0,0 +1 @@
1
+ <%= render template: 'articles/new', locals: { errors: {} } %>
@@ -0,0 +1,5 @@
1
+ <entry>
2
+ <% articles.each do |article| %>
3
+ <title><%= article.title %></title>
4
+ <% end %>
5
+ <entry>