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