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.
@@ -4,20 +4,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'hanami/view/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "hanami-view"
7
+ spec.name = 'hanami-view'
8
8
  spec.version = Hanami::View::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
9
+ spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
10
+ spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
11
+ spec.description = %q{View layer for Hanami}
12
+ spec.summary = %q{View layer for Hanami, with a separation between views and templates}
13
+ spec.homepage = 'http://hanamirb.org'
14
+ spec.license = 'MIT'
11
15
 
12
- spec.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
16
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-view.gemspec`.split($/)
17
+ spec.executables = []
18
+ spec.test_files = spec.files.grep(%r{^(test)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 2.0.0'
15
21
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
22
+ spec.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.1'
23
+ spec.add_runtime_dependency 'hanami-utils', '~> 0.7'
20
24
 
21
- spec.add_development_dependency "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency 'bundler', '~> 1.5'
26
+ spec.add_development_dependency 'minitest', '~> 5'
27
+ spec.add_development_dependency 'rake', '~> 10'
23
28
  end
@@ -0,0 +1 @@
1
+ require 'hanami/view'
@@ -0,0 +1,142 @@
1
+ require 'hanami/utils/class_attribute'
2
+ require 'hanami/view/rendering/layout_registry'
3
+ require 'hanami/view/rendering/layout_scope'
4
+ require 'hanami/view/rendering/null_layout'
5
+
6
+ module Hanami
7
+ # Layout
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see Hanami::Layout::ClassMethods
12
+ module Layout
13
+ # Register a layout
14
+ #
15
+ # @api private
16
+ # @since 0.1.0
17
+ #
18
+ # @example
19
+ # require 'hanami/view'
20
+ #
21
+ # class ApplicationLayout
22
+ # include Hanami::Layout
23
+ # end
24
+ def self.included(base)
25
+ conf = Hanami::View::Configuration.for(base)
26
+ conf.add_layout(base)
27
+
28
+ base.class_eval do
29
+ extend Hanami::View::Dsl.dup
30
+ extend ClassMethods
31
+
32
+ include Utils::ClassAttribute
33
+ class_attribute :configuration
34
+
35
+ self.configuration = conf.duplicate
36
+ end
37
+
38
+ conf.copy!(base)
39
+ end
40
+
41
+ # Class level API
42
+ #
43
+ # @since 0.1.0
44
+ module ClassMethods
45
+ # Template name suffix
46
+ #
47
+ # @api private
48
+ # @since 0.1.0
49
+ #
50
+ # @see Hanami::Layout::ClassMethods#suffix
51
+ # @see Hanami::Layout::ClassMethods#template
52
+ SUFFIX = '_layout'.freeze
53
+
54
+ # A registry that holds all the registered layouts.
55
+ #
56
+ # @api private
57
+ # @since 0.1.0
58
+ #
59
+ # @see Hanami::View::Rendering::LayoutRegistry
60
+ def registry
61
+ @registry ||= View::Rendering::LayoutRegistry.new(self)
62
+ end
63
+
64
+ # Template name
65
+ #
66
+ # @api private
67
+ # @since 0.1.0
68
+ #
69
+ # @see Hanami::Layout::ClassMethods#SUFFIX
70
+ # @see Hanami::Layout::ClassMethods#suffix
71
+ #
72
+ # @example
73
+ # # Given a template 'templates/application.html.erb'
74
+ #
75
+ # class ApplicationLayout
76
+ # include Hanami::Layout
77
+ # end
78
+ #
79
+ # ApplicationLayout.template # => 'application'
80
+ def template
81
+ super.sub(suffix, '')
82
+ end
83
+
84
+ # Template name suffix
85
+ #
86
+ # @api private
87
+ # @since 0.1.0
88
+ #
89
+ # @see Hanami::Layout::ClassMethods#SUFFIX
90
+ # @see Hanami::Layout::ClassMethods#template
91
+ def suffix
92
+ SUFFIX
93
+ end
94
+
95
+ protected
96
+ # Loading mechanism hook.
97
+ #
98
+ # @api private
99
+ # @since 0.1.0
100
+ #
101
+ # @see Hanami::View.load!
102
+ def load!
103
+ registry.freeze
104
+ configuration.freeze
105
+ end
106
+ end
107
+
108
+ # Initialize a layout
109
+ #
110
+ # @param scope [Hanami::View::Rendering::Scope] view rendering scope
111
+ # @param rendered [String] the output of the view rendering process
112
+ #
113
+ # @api private
114
+ # @since 0.1.0
115
+ #
116
+ # @see Hanami::View::Rendering#render
117
+ def initialize(scope, rendered)
118
+ @scope, @rendered = View::Rendering::LayoutScope.new(self, scope), rendered
119
+ end
120
+
121
+ # Render the layout
122
+ #
123
+ # @return [String] the output of the rendering process
124
+ #
125
+ # @api private
126
+ # @since 0.1.0
127
+ #
128
+ # @see Hanami::View::Rendering#render
129
+ def render
130
+ template.render(@scope, &Proc.new{@rendered})
131
+ end
132
+
133
+ protected
134
+ # The template for the current format
135
+ #
136
+ # @api private
137
+ # @since 0.1.0
138
+ def template
139
+ self.class.registry.resolve({format: @scope.format})
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,126 @@
1
+ require 'hanami/view/escape'
2
+
3
+ module Hanami
4
+ # Presenter pattern implementation
5
+ #
6
+ # It delegates to the wrapped object the missing method invocations.
7
+ #
8
+ # The output of concrete and delegated methods is escaped as XSS prevention.
9
+ #
10
+ # @since 0.1.0
11
+ #
12
+ # @example Basic usage
13
+ # require 'hanami/view'
14
+ #
15
+ # class Map
16
+ # attr_reader :locations
17
+ #
18
+ # def initialize(locations)
19
+ # @locations = locations
20
+ # end
21
+ #
22
+ # def location_names
23
+ # @locations.join(', ')
24
+ # end
25
+ # end
26
+ #
27
+ # class MapPresenter
28
+ # include Hanami::Presenter
29
+ #
30
+ # def count
31
+ # locations.count
32
+ # end
33
+ #
34
+ # def location_names
35
+ # super.upcase
36
+ # end
37
+ #
38
+ # def inspect_object
39
+ # @object.inspect
40
+ # end
41
+ # end
42
+ #
43
+ # map = Map.new(['Rome', 'Boston'])
44
+ # presenter = MapPresenter.new(map)
45
+ #
46
+ # # access a map method
47
+ # puts presenter.locations # => ['Rome', 'Boston']
48
+ #
49
+ # # access presenter concrete methods
50
+ # puts presenter.count # => 1
51
+ #
52
+ # # uses super to access original object implementation
53
+ # puts presenter.location_names # => 'ROME, BOSTON'
54
+ #
55
+ # # it has private access to the original object
56
+ # puts presenter.inspect_object # => #<Map:0x007fdeada0b2f0 @locations=["Rome", "Boston"]>
57
+ #
58
+ # @example Escape
59
+ # require 'hanami/view'
60
+ #
61
+ # User = Struct.new(:first_name, :last_name)
62
+ #
63
+ # class UserPresenter
64
+ # include Hanami::Presenter
65
+ #
66
+ # def full_name
67
+ # [first_name, last_name].join(' ')
68
+ # end
69
+ #
70
+ # def raw_first_name
71
+ # _raw first_name
72
+ # end
73
+ # end
74
+ #
75
+ # first_name = '<script>alert('xss')</script>'
76
+ #
77
+ # user = User.new(first_name, nil)
78
+ # presenter = UserPresenter.new(user)
79
+ #
80
+ # presenter.full_name
81
+ # # => "&lt;script&gt;alert(&apos;xss&apos;)&lt;&#x2F;script&gt;"
82
+ #
83
+ # presenter.raw_full_name
84
+ # # => "<script>alert('xss')</script>"
85
+ module Presenter
86
+ # Inject escape logic into the given class.
87
+ #
88
+ # @since 0.4.0
89
+ # @api private
90
+ #
91
+ # @see Hanami::View::Escape
92
+ def self.included(base)
93
+ base.extend ::Hanami::View::Escape
94
+ end
95
+
96
+ # Initialize the presenter
97
+ #
98
+ # @param object [Object] the object to present
99
+ #
100
+ # @since 0.1.0
101
+ def initialize(object)
102
+ @object = object
103
+ end
104
+
105
+ protected
106
+ # Override Ruby's method_missing
107
+ #
108
+ # @api private
109
+ # @since 0.1.0
110
+ def method_missing(m, *args, &blk)
111
+ if @object.respond_to?(m)
112
+ ::Hanami::View::Escape.html(@object.__send__(m, *args, &blk))
113
+ else
114
+ super
115
+ end
116
+ end
117
+
118
+ # Override Ruby's respond_to_missing? in order to support proper delegation
119
+ #
120
+ # @api private
121
+ # @since 0.3.0
122
+ def respond_to_missing?(m, include_private = false)
123
+ @object.respond_to?(m, include_private)
124
+ end
125
+ end
126
+ end
@@ -1,7 +1,264 @@
1
- require "hanami/view/version"
1
+ require 'set'
2
+ require 'pathname'
3
+ require 'hanami/utils/class_attribute'
4
+ require 'hanami/view/version'
5
+ require 'hanami/view/configuration'
6
+ require 'hanami/view/inheritable'
7
+ require 'hanami/view/rendering'
8
+ require 'hanami/view/escape'
9
+ require 'hanami/view/dsl'
10
+ require 'hanami/view/errors'
11
+ require 'hanami/layout'
12
+ require 'hanami/presenter'
2
13
 
3
14
  module Hanami
15
+ # View
16
+ #
17
+ # @since 0.1.0
4
18
  module View
5
- # Your code goes here...
19
+ include Utils::ClassAttribute
20
+ # Framework configuration
21
+ #
22
+ # @since 0.2.0
23
+ # @api private
24
+ class_attribute :configuration
25
+ self.configuration = Configuration.new
26
+
27
+ # Configure the framework.
28
+ # It yields the given block in the context of the configuration
29
+ #
30
+ # @param blk [Proc] the configuration block
31
+ #
32
+ # @since 0.2.0
33
+ #
34
+ # @see Hanami::View::Configuration
35
+ #
36
+ # @example
37
+ # require 'hanami/view'
38
+ #
39
+ # Hanami::View.configure do
40
+ # root '/path/to/root'
41
+ # end
42
+ def self.configure(&blk)
43
+ configuration.instance_eval(&blk)
44
+ end
45
+
46
+ # Duplicate Hanami::View in order to create a new separated instance
47
+ # of the framework.
48
+ #
49
+ # The new instance of the framework will be completely decoupled from the
50
+ # original. It will inherit the configuration, but all the changes that
51
+ # happen after the duplication, won't be reflected on the other copies.
52
+ #
53
+ # @return [Module] a copy of Hanami::View
54
+ #
55
+ # @since 0.2.0
56
+ # @api private
57
+ #
58
+ # @example Basic usage
59
+ # require 'hanami/view'
60
+ #
61
+ # module MyApp
62
+ # View = Hanami::View.dupe
63
+ # end
64
+ #
65
+ # MyApp::View == Hanami::View # => false
66
+ #
67
+ # MyApp::View.configuration ==
68
+ # Hanami::View.configuration # => false
69
+ #
70
+ # @example Inheriting configuration
71
+ # require 'hanami/view'
72
+ #
73
+ # Hanami::View.configure do
74
+ # root '/path/to/root'
75
+ # end
76
+ #
77
+ # module MyApp
78
+ # View = Hanami::View.dupe
79
+ # end
80
+ #
81
+ # module MyApi
82
+ # View = Hanami::View.dupe
83
+ # View.configure do
84
+ # root '/another/root'
85
+ # end
86
+ # end
87
+ #
88
+ # Hanami::View.configuration.root # => #<Pathname:/path/to/root>
89
+ # MyApp::View.configuration.root # => #<Pathname:/path/to/root>
90
+ # MyApi::View.configuration.root # => #<Pathname:/another/root>
91
+ def self.dupe
92
+ dup.tap do |duplicated|
93
+ duplicated.configuration = configuration.duplicate
94
+ end
95
+ end
96
+
97
+ # Duplicate the framework and generate modules for the target application
98
+ #
99
+ # @param mod [Module] the Ruby namespace of the application
100
+ # @param views [String] the optional namespace where the application's
101
+ # views will live
102
+ # @param blk [Proc] an optional block to configure the framework
103
+ #
104
+ # @return [Module] a copy of Hanami::View
105
+ #
106
+ # @since 0.2.0
107
+ #
108
+ # @see Hanami::View#dupe
109
+ # @see Hanami::View::Configuration
110
+ # @see Hanami::View::Configuration#namespace
111
+ #
112
+ # @example Basic usage
113
+ # require 'hanami/view'
114
+ #
115
+ # module MyApp
116
+ # View = Hanami::View.duplicate(self)
117
+ # end
118
+ #
119
+ # # It will:
120
+ # #
121
+ # # 1. Generate MyApp::View
122
+ # # 2. Generate MyApp::Layout
123
+ # # 3. Generate MyApp::Presenter
124
+ # # 4. Generate MyApp::Views
125
+ # # 5. Configure MyApp::Views as the default namespace for views
126
+ #
127
+ # module MyApp::Views::Dashboard
128
+ # class Index
129
+ # include MyApp::View
130
+ # end
131
+ # end
132
+ #
133
+ # @example Compare code
134
+ # require 'hanami/view'
135
+ #
136
+ # module MyApp
137
+ # View = Hanami::View.duplicate(self) do
138
+ # # ...
139
+ # end
140
+ # end
141
+ #
142
+ # # it's equivalent to:
143
+ #
144
+ # module MyApp
145
+ # View = Hanami::View.dupe
146
+ # Layout = Hanami::Layout.dup
147
+ #
148
+ # module Views
149
+ # end
150
+ #
151
+ # View.configure do
152
+ # namespace 'MyApp::Views'
153
+ # end
154
+ #
155
+ # View.configure do
156
+ # # ...
157
+ # end
158
+ # end
159
+ #
160
+ # @example Custom views module
161
+ # require 'hanami/view
162
+ #
163
+ # module MyApp
164
+ # View = Hanami::View.duplicate(self, 'Vs')
165
+ # end
166
+ #
167
+ # defined?(MyApp::Views) # => nil
168
+ # defined?(MyApp::Vs) # => "constant"
169
+ #
170
+ # # Developers can namespace views under Vs
171
+ # module MyApp::Vs::Dashboard
172
+ # # ...
173
+ # end
174
+ #
175
+ # @example Nil views module
176
+ # require 'hanami/view'
177
+ #
178
+ # module MyApp
179
+ # View = Hanami::View.duplicate(self, nil)
180
+ # end
181
+ #
182
+ # defined?(MyApp::Views) # => nil
183
+ #
184
+ # # Developers can namespace views under MyApp
185
+ # module MyApp
186
+ # # ...
187
+ # end
188
+ #
189
+ # @example Block usage
190
+ # require 'hanami/view'
191
+ #
192
+ # module MyApp
193
+ # View = Hanami::View.duplicate(self) do
194
+ # root '/path/to/root'
195
+ # end
196
+ # end
197
+ #
198
+ # Hanami::View.configuration.root # => #<Pathname:.>
199
+ # MyApp::View.configuration.root # => #<Pathname:/path/to/root>
200
+ def self.duplicate(mod, views = 'Views', &blk)
201
+ dupe.tap do |duplicated|
202
+ mod.module_eval %{ module #{ views }; end } if views
203
+ mod.module_eval %{
204
+ Layout = Hanami::Layout.dup
205
+ Presenter = Hanami::Presenter.dup
206
+ }
207
+
208
+ duplicated.configure do
209
+ namespace [mod, views].compact.join '::'
210
+ end
211
+
212
+ duplicated.configure(&blk) if block_given?
213
+ end
214
+ end
215
+
216
+ # Override Ruby's hook for modules.
217
+ # It includes basic Hanami::View modules to the given Class.
218
+ # It sets a copy of the framework configuration
219
+ #
220
+ # @param base [Class] the target view
221
+ #
222
+ # @since 0.1.0
223
+ # @api private
224
+ #
225
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
226
+ #
227
+ # @see Hanami::View::Dsl
228
+ # @see Hanami::View::Inheritable
229
+ # @see Hanami::View::Rendering
230
+ #
231
+ # @example
232
+ # require 'hanami/view'
233
+ #
234
+ # class IndexView
235
+ # include Hanami::View
236
+ # end
237
+ def self.included(base)
238
+ conf = self.configuration
239
+ conf.add_view(base)
240
+
241
+ base.class_eval do
242
+ extend Inheritable
243
+ extend Dsl
244
+ extend Rendering
245
+ extend Escape
246
+
247
+ include Utils::ClassAttribute
248
+ class_attribute :configuration
249
+
250
+ self.configuration = conf.duplicate
251
+ end
252
+
253
+ conf.copy!(base)
254
+ end
255
+
256
+ # Load the framework
257
+ #
258
+ # @since 0.1.0
259
+ # @api private
260
+ def self.load!
261
+ configuration.load!
262
+ end
6
263
  end
7
264
  end