hanami-view 0.0.0 → 0.6.0

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.
@@ -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