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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/LICENSE.md +22 -0
- data/README.md +826 -7
- data/hanami-view.gemspec +17 -12
- data/lib/hanami-view.rb +1 -0
- data/lib/hanami/layout.rb +142 -0
- data/lib/hanami/presenter.rb +126 -0
- data/lib/hanami/view.rb +259 -2
- data/lib/hanami/view/configuration.rb +464 -0
- data/lib/hanami/view/dsl.rb +346 -0
- data/lib/hanami/view/errors.rb +47 -0
- data/lib/hanami/view/escape.rb +180 -0
- data/lib/hanami/view/inheritable.rb +54 -0
- data/lib/hanami/view/rendering.rb +265 -0
- data/lib/hanami/view/rendering/layout_finder.rb +128 -0
- data/lib/hanami/view/rendering/layout_registry.rb +63 -0
- data/lib/hanami/view/rendering/layout_scope.rb +240 -0
- data/lib/hanami/view/rendering/null_layout.rb +52 -0
- data/lib/hanami/view/rendering/null_template.rb +83 -0
- data/lib/hanami/view/rendering/partial.rb +29 -0
- data/lib/hanami/view/rendering/partial_finder.rb +73 -0
- data/lib/hanami/view/rendering/registry.rb +128 -0
- data/lib/hanami/view/rendering/scope.rb +88 -0
- data/lib/hanami/view/rendering/template.rb +67 -0
- data/lib/hanami/view/rendering/template_finder.rb +53 -0
- data/lib/hanami/view/rendering/template_name.rb +37 -0
- data/lib/hanami/view/rendering/templates_finder.rb +129 -0
- data/lib/hanami/view/rendering/view_finder.rb +37 -0
- data/lib/hanami/view/template.rb +43 -0
- data/lib/hanami/view/version.rb +4 -1
- metadata +91 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -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
|
+
# # <script>alert('first_name')</script>
|
113
|
+
# # </div>
|
114
|
+
# # <div id="last_name">
|
115
|
+
# # <script>alert('last_name')</script>
|
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
|