hanami-view 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,346 @@
1
+ require 'hanami/view/rendering/template_name'
2
+ require 'hanami/view/rendering/layout_finder'
3
+
4
+ module Hanami
5
+ module View
6
+ # Class level DSL
7
+ #
8
+ # @since 0.1.0
9
+ module Dsl
10
+ # When a value is given, specify a templates root path for the view.
11
+ # Otherwise, it returns templates root path.
12
+ #
13
+ # When not initialized, it will return the global value from `Hanami::View.root`.
14
+ #
15
+ # @param value [String] the templates root for this view
16
+ #
17
+ # @return [Pathname] the specified root for this view or the global value
18
+ #
19
+ # @since 0.1.0
20
+ #
21
+ # @example Default usage
22
+ # require 'hanami/view'
23
+ #
24
+ # module Articles
25
+ # class Show
26
+ # include Hanami::View
27
+ # end
28
+ # end
29
+ #
30
+ # Hanami::View.configuration.root # => 'app/templates'
31
+ # Articles::Show.root # => 'app/templates'
32
+ #
33
+ # @example Custom root
34
+ # require 'hanami/view'
35
+ #
36
+ # module Articles
37
+ # class Show
38
+ # include Hanami::View
39
+ # root 'path/to/articles/templates'
40
+ # end
41
+ # end
42
+ #
43
+ # Hanami::View.configuration.root # => 'app/templates'
44
+ # Articles::Show.root # => 'path/to/articles/templates'
45
+ def root(value = nil)
46
+ if value.nil?
47
+ configuration.root
48
+ else
49
+ configuration.root(value)
50
+ end
51
+ end
52
+
53
+ # When a value is given, specify the handled format.
54
+ # Otherwise, it returns the previously specified format.
55
+ #
56
+ # @param value [Symbol] the format
57
+ #
58
+ # @return [Symbol, nil] the specified format for this view, if set
59
+ #
60
+ # @since 0.1.0
61
+ #
62
+ # @example
63
+ # require 'hanami/view'
64
+ #
65
+ # module Articles
66
+ # class Show
67
+ # include Hanami::View
68
+ # end
69
+ #
70
+ # class JsonShow < Show
71
+ # format :json
72
+ # end
73
+ # end
74
+ #
75
+ # Articles::Show.format # => nil
76
+ # Articles::JsonShow.format # => :json
77
+ def format(value = nil)
78
+ if value.nil?
79
+ @format
80
+ else
81
+ @format = value
82
+ end
83
+ end
84
+
85
+ # When a value is given, specify the relative path to the template.
86
+ # Otherwise, it returns the name that follows Hanami::View conventions.
87
+ #
88
+ # @param value [String] relative template path
89
+ #
90
+ # @return [String] the specified template for this view or the name
91
+ # that follows the convention
92
+ #
93
+ # @since 0.1.0
94
+ #
95
+ # @example Default usage
96
+ # require 'hanami/view'
97
+ #
98
+ # module Articles
99
+ # class Show
100
+ # include Hanami::View
101
+ # end
102
+ #
103
+ # class JsonShow < Show
104
+ # format :json
105
+ # end
106
+ # end
107
+ #
108
+ # Articles::Show.template # => 'articles/show'
109
+ # Articles::JsonShow.template # => 'articles/show'
110
+ #
111
+ # @example Custom template
112
+ # require 'hanami/view'
113
+ #
114
+ # module Articles
115
+ # class Show
116
+ # include Hanami::View
117
+ # template 'articles/single_article'
118
+ # end
119
+ #
120
+ # class JsonShow < Show
121
+ # format :json
122
+ # end
123
+ # end
124
+ #
125
+ # Articles::Show.template # => 'articles/single_article'
126
+ # Articles::JsonShow.template # => 'articles/single_article'
127
+ #
128
+ # @example With namespace
129
+ # require 'hanami/view'
130
+ #
131
+ # module Furnitures
132
+ # View = Hanami::View.generate(self)
133
+ #
134
+ # class Standalone
135
+ # include Furnitures::View
136
+ # end
137
+ #
138
+ # module Catalog
139
+ # class Index
140
+ # Furnitures::View
141
+ # end
142
+ # end
143
+ # end
144
+ #
145
+ # Furnitures::Standalone.template # => 'standalone'
146
+ # Furnitures::Catalog::Index.template # => 'catalog/index'
147
+ #
148
+ # @example With nested namespace
149
+ # require 'hanami/view'
150
+ #
151
+ # module Frontend
152
+ # View = Hanami::View.generate(self)
153
+ #
154
+ # class StandaloneView
155
+ # include Frontend::View
156
+ # end
157
+ #
158
+ # module Views
159
+ # class Standalone
160
+ # include Frontend::View
161
+ # end
162
+ #
163
+ # module Sessions
164
+ # class New
165
+ # include Frontend::View
166
+ # end
167
+ # end
168
+ # end
169
+ # end
170
+ #
171
+ # Frontend::StandaloneView.template # => 'standalone_view'
172
+ # Frontend::Views::Standalone.template # => 'standalone'
173
+ # Frontend::Views::Sessions::New.template # => 'sessions/new'
174
+ #
175
+ # @example With deeply nested namespace
176
+ # require 'hanami/view'
177
+ #
178
+ # module Bookshelf
179
+ # module Web
180
+ # View = Hanami::View.generate(self)
181
+ #
182
+ # module Views
183
+ # module Books
184
+ # class Show
185
+ # include Bookshelf::Web::View
186
+ # end
187
+ # end
188
+ # end
189
+ # end
190
+ #
191
+ # module Api
192
+ # View = Hanami::View.generate(self)
193
+ #
194
+ # module Views
195
+ # module Books
196
+ # class Show
197
+ # include Bookshelf::Api::View
198
+ # end
199
+ # end
200
+ # end
201
+ # end
202
+ # end
203
+ #
204
+ # Bookshelf::Web::Views::Books::Index.template # => 'books/index'
205
+ # Bookshelf::Api::Views::Books::Index.template # => 'books/index'
206
+ def template(value = nil)
207
+ if value.nil?
208
+ @template ||= Rendering::TemplateName.new(name, configuration.namespace).to_s
209
+ else
210
+ @template = value
211
+ end
212
+ end
213
+
214
+ # When a value is given, it specifies the layout.
215
+ # When false is given, Hanami::View::Rendering::NullLayout is returned.
216
+ # Otherwise, it returns the previously specified layout.
217
+ #
218
+ # When the global configuration is set (`Hanami::View.layout=`), after the
219
+ # loading process, it will return that layout if not otherwise specified.
220
+ #
221
+ # @param value [Symbol, FalseClass, nil] the layout name
222
+ #
223
+ # @return [Symbol, nil] the specified layout for this view, if set
224
+ #
225
+ # @since 0.1.0
226
+ #
227
+ # @see Hanami::Layout
228
+ #
229
+ # @example Default usage
230
+ # require 'hanami/view'
231
+ #
232
+ # module Articles
233
+ # class Show
234
+ # include Hanami::View
235
+ # end
236
+ # end
237
+ #
238
+ # Articles::Show.layout # => nil
239
+ #
240
+ # @example Custom layout
241
+ # require 'hanami/view'
242
+ #
243
+ # class ArticlesLayout
244
+ # include Hanami::Layout
245
+ # end
246
+ #
247
+ # module Articles
248
+ # class Show
249
+ # include Hanami::View
250
+ # layout :articles
251
+ # end
252
+ # end
253
+ #
254
+ # Articles::Show.layout # => :articles
255
+ #
256
+ # @example Global configuration
257
+ # require 'hanami/view'
258
+ #
259
+ # class ApplicationLayout
260
+ # include Hanami::Layout
261
+ # end
262
+ #
263
+ # module Articles
264
+ # class Show
265
+ # include Hanami::View
266
+ # end
267
+ # end
268
+ #
269
+ # Hanami::View.layout = :application
270
+ # Articles::Show.layout # => nil
271
+ #
272
+ # Hanami::View.load!
273
+ # Articles::Show.layout # => :application
274
+ #
275
+ # @example Global configuration with custom layout
276
+ # require 'hanami/view'
277
+ #
278
+ # class ApplicationLayout
279
+ # include Hanami::Layout
280
+ # end
281
+ #
282
+ # class ArticlesLayout
283
+ # include Hanami::Layout
284
+ # end
285
+ #
286
+ # module Articles
287
+ # class Show
288
+ # include Hanami::View
289
+ # layout :articles
290
+ # end
291
+ # end
292
+ #
293
+ # Hanami::View.layout = :application
294
+ # Articles::Show.layout # => :articles
295
+ #
296
+ # Hanami::View.load!
297
+ # Articles::Show.layout # => :articles
298
+ #
299
+ # @example Disable layout for the view
300
+ # require 'hanami/view'
301
+ #
302
+ # class ApplicationLayout
303
+ # include Hanami::Layout
304
+ # end
305
+ #
306
+ # module Articles
307
+ # class Show
308
+ # include Hanami::View
309
+ # layout false
310
+ # end
311
+ # end
312
+ #
313
+ # Hanami::View.load!
314
+ # Articles::Show.layout # => Hanami::View::Rendering::NullLayout
315
+ def layout(value = nil)
316
+ if value.nil?
317
+ @_layout ||= Rendering::LayoutFinder.find(@layout || configuration.layout, configuration.namespace)
318
+ elsif !value
319
+ @layout = Hanami::View::Rendering::NullLayout
320
+ else
321
+ @layout = value
322
+ end
323
+ end
324
+
325
+ protected
326
+
327
+ # Loading mechanism hook.
328
+ #
329
+ # @api private
330
+ # @since 0.1.0
331
+ #
332
+ # @see Hanami::View.load!
333
+ def load!
334
+ super
335
+
336
+ views.each do |v|
337
+ v.root.freeze
338
+ v.format.freeze
339
+ v.template.freeze
340
+ v.layout#.freeze
341
+ v.configuration.freeze
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,47 @@
1
+ module Hanami
2
+ module View
3
+ # @since 0.5.0
4
+ class Error < ::StandardError
5
+ end
6
+
7
+ # Missing template error
8
+ #
9
+ # This is raised at the runtime when Hanami::View cannot find a template for
10
+ # the requested format.
11
+ #
12
+ # We can't raise this error during the loading phase, because at that time
13
+ # we don't know if a view implements its own rendering policy.
14
+ # A view is allowed to override `#render`, and this scenario can make the
15
+ # presence of a template useless. One typical example is the usage of a
16
+ # serializer that returns the output string, without rendering a template.
17
+ #
18
+ # @since 0.1.0
19
+ class MissingTemplateError < Error
20
+ def initialize(template, format)
21
+ super("Can't find template '#{ template }' for '#{ format }' format.")
22
+ end
23
+ end
24
+
25
+ # Missing format error
26
+ #
27
+ # This is raised at the runtime when rendering context lacks of the :format
28
+ # key.
29
+ #
30
+ # @since 0.1.0
31
+ #
32
+ # @see Hanami::View::Rendering#render
33
+ class MissingFormatError < Error
34
+ end
35
+
36
+ # Missing template layout error
37
+ #
38
+ # This is raised at the runtime when Hanami::Layout cannot find it's template.
39
+ #
40
+ # @since 0.5.0
41
+ class MissingTemplateLayoutError < Error
42
+ def initialize(template)
43
+ super("Can't find layout template '#{ template }'")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,180 @@
1
+ require 'hanami/utils/escape'
2
+ require 'hanami/presenter'
3
+
4
+ module Hanami
5
+ module View
6
+ # Auto escape logic for views and presenters.
7
+ #
8
+ # @since 0.4.0
9
+ module Escape
10
+ module InstanceMethods
11
+ private
12
+ # Mark the given string as safe to render.
13
+ #
14
+ # !!! ATTENTION !!! This may open your application to XSS attacks.
15
+ #
16
+ # @param string [String] the input string
17
+ #
18
+ # @return [Hanami::Utils::Escape::SafeString] the string marked as safe
19
+ #
20
+ # @since 0.4.0
21
+ # @api public
22
+ #
23
+ # @example View usage
24
+ # require 'hanami/view'
25
+ #
26
+ # User = Struct.new(:name)
27
+ #
28
+ # module Users
29
+ # class Show
30
+ # include Hanami::View
31
+ #
32
+ # def user_name
33
+ # _raw user.name
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # # ERB template
39
+ # # <div id="user_name"><%= user_name %></div>
40
+ #
41
+ # user = User.new("<script>alert('xss')</script>")
42
+ # html = Users::Show.render(format: :html, user: user)
43
+ #
44
+ # html # => <div id="user_name"><script>alert('xss')</script></div>
45
+ #
46
+ # @example Presenter usage
47
+ # require 'hanami/view'
48
+ #
49
+ # User = Struct.new(:name)
50
+ #
51
+ # class UserPresenter
52
+ # include Hanami::Presenter
53
+ #
54
+ # def name
55
+ # _raw @object.name
56
+ # end
57
+ # end
58
+ #
59
+ # user = User.new("<script>alert('xss')</script>")
60
+ # presenter = UserPresenter.new(user)
61
+ #
62
+ # presenter.name # => "<script>alert('xss')</script>"
63
+ def _raw(string)
64
+ ::Hanami::Utils::Escape::SafeString.new(string)
65
+ end
66
+
67
+ # Force the output escape for the given object
68
+ #
69
+ # @param object [Object] the input object
70
+ #
71
+ # @return [Hanami::View::Escape::Presenter] a presenter with output
72
+ # autoescape
73
+ #
74
+ # @since 0.4.0
75
+ # @api public
76
+ #
77
+ # @see Hanami::View::Escape::Presenter
78
+ #
79
+ # @example View usage
80
+ # require 'hanami/view'
81
+ #
82
+ # User = Struct.new(:first_name, :last_name)
83
+ #
84
+ # module Users
85
+ # class Show
86
+ # include Hanami::View
87
+ #
88
+ # def user
89
+ # _escape locals[:user]
90
+ # end
91
+ # end
92
+ # end
93
+ #
94
+ # # ERB template:
95
+ # #
96
+ # # <div id="first_name">
97
+ # # <%= user.first_name %>
98
+ # # </div>
99
+ # # <div id="last_name">
100
+ # # <%= user.last_name %>
101
+ # # </div>
102
+ #
103
+ # first_name = "<script>alert('first_name')</script>"
104
+ # last_name = "<script>alert('last_name')</script>"
105
+ #
106
+ # user = User.new(first_name, last_name)
107
+ # html = Users::Show.render(format: :html, user: user)
108
+ #
109
+ # html
110
+ # # =>
111
+ # # <div id="first_name">
112
+ # # &lt;script&gt;alert(&apos;first_name&apos;)&lt;&#x2F;script&gt;
113
+ # # </div>
114
+ # # <div id="last_name">
115
+ # # &lt;script&gt;alert(&apos;last_name&apos;)&lt;&#x2F;script&gt;
116
+ # # </div>
117
+ def _escape(object)
118
+ ::Hanami::View::Escape::Presenter.new(object)
119
+ end
120
+ end
121
+
122
+ # Auto escape presenter
123
+ #
124
+ # @since 0.4.0
125
+ # @api private
126
+ #
127
+ # @see Hanami::View::Escape::InstanceMethods#_escape
128
+ class Presenter
129
+ include ::Hanami::Presenter
130
+ end
131
+
132
+ # Escape the given input if it's a string, otherwise return the oject as it is.
133
+ #
134
+ # @param input [Object] the input
135
+ #
136
+ # @return [Object,String] the escaped string or the given object
137
+ #
138
+ # @since 0.4.0
139
+ # @api private
140
+ def self.html(input)
141
+ case input
142
+ when String
143
+ Utils::Escape.html(input)
144
+ else
145
+ input
146
+ end
147
+ end
148
+
149
+ # Module extended override
150
+ #
151
+ # @since 0.4.0
152
+ # @api private
153
+ def self.extended(base)
154
+ base.class_eval do
155
+ include ::Hanami::Utils::ClassAttribute
156
+ include ::Hanami::View::Escape::InstanceMethods
157
+
158
+ class_attribute :autoescape_methods
159
+ self.autoescape_methods = {}
160
+ end
161
+ end
162
+
163
+ # Wraps concrete view methods with escape logic.
164
+ #
165
+ # @since 0.4.0
166
+ # @api private
167
+ def method_added(method_name)
168
+ unless autoescape_methods[method_name]
169
+ prepend Module.new {
170
+ module_eval %{
171
+ def #{ method_name }(*args, &blk); ::Hanami::View::Escape.html super; end
172
+ }
173
+ }
174
+
175
+ autoescape_methods[method_name] = true
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end