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