hanami-view 2.0.0.alpha7 → 2.1.0.beta1

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +15 -3
  4. data/hanami-view.gemspec +5 -3
  5. data/lib/hanami/view/cache.rb +16 -0
  6. data/lib/hanami/view/context.rb +12 -70
  7. data/lib/hanami/view/context_helpers/content_helpers.rb +5 -5
  8. data/lib/hanami/view/decorated_attributes.rb +2 -2
  9. data/lib/hanami/view/erb/engine.rb +27 -0
  10. data/lib/hanami/view/erb/filters/block.rb +44 -0
  11. data/lib/hanami/view/erb/filters/trimming.rb +42 -0
  12. data/lib/hanami/view/erb/parser.rb +161 -0
  13. data/lib/hanami/view/erb/template.rb +30 -0
  14. data/lib/hanami/view/errors.rb +5 -7
  15. data/lib/hanami/view/exposure.rb +23 -17
  16. data/lib/hanami/view/exposures.rb +22 -13
  17. data/lib/hanami/view/helpers/escape_helper.rb +221 -0
  18. data/lib/hanami/view/helpers/number_formatting_helper.rb +182 -0
  19. data/lib/hanami/view/helpers/tag_helper/tag_builder.rb +230 -0
  20. data/lib/hanami/view/helpers/tag_helper.rb +210 -0
  21. data/lib/hanami/view/html.rb +104 -0
  22. data/lib/hanami/view/html_safe_string_buffer.rb +46 -0
  23. data/lib/hanami/view/part.rb +13 -15
  24. data/lib/hanami/view/part_builder.rb +68 -108
  25. data/lib/hanami/view/path.rb +4 -31
  26. data/lib/hanami/view/renderer.rb +36 -44
  27. data/lib/hanami/view/rendering.rb +42 -0
  28. data/lib/hanami/view/{render_environment_missing.rb → rendering_missing.rb} +8 -13
  29. data/lib/hanami/view/scope.rb +15 -16
  30. data/lib/hanami/view/scope_builder.rb +42 -78
  31. data/lib/hanami/view/tilt/haml_adapter.rb +40 -0
  32. data/lib/hanami/view/tilt/slim_adapter.rb +40 -0
  33. data/lib/hanami/view/tilt.rb +22 -46
  34. data/lib/hanami/view/version.rb +1 -1
  35. data/lib/hanami/view.rb +351 -26
  36. metadata +64 -29
  37. data/LICENSE +0 -20
  38. data/lib/hanami/view/application_configuration.rb +0 -77
  39. data/lib/hanami/view/application_context.rb +0 -98
  40. data/lib/hanami/view/render_environment.rb +0 -62
  41. data/lib/hanami/view/standalone_view.rb +0 -400
  42. data/lib/hanami/view/tilt/erb.rb +0 -26
  43. data/lib/hanami/view/tilt/erbse.rb +0 -21
  44. data/lib/hanami/view/tilt/haml.rb +0 -26
@@ -1,400 +0,0 @@
1
- require_relative "scope"
2
-
3
- module Hanami
4
- class View
5
- module StandaloneView
6
- def self.included(klass)
7
- klass.extend ClassMethods
8
- klass.include InstanceMethods
9
- end
10
-
11
- module ClassMethods
12
- # @api private
13
- def inherited(klass)
14
- super
15
-
16
- exposures.each do |name, exposure|
17
- klass.exposures.import(name, exposure)
18
- end
19
- end
20
-
21
- # @!group Exposures
22
-
23
- # @!macro [new] exposure_options
24
- # @param options [Hash] the exposure's options
25
- # @option options [Boolean] :layout expose this value to the layout (defaults to false)
26
- # @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
27
- # true)
28
- # @option options [Symbol, Class] :as an alternative name or class to use when finding a
29
- # matching Part
30
-
31
- # @overload expose(name, **options, &block)
32
- # Define a value to be passed to the template. The return value of the
33
- # block will be decorated by a matching Part and passed to the template.
34
- #
35
- # The block will be evaluated with the view instance as its `self`. The
36
- # block's parameters will determine what it is given:
37
- #
38
- # - To receive other exposure values, provide positional parameters
39
- # matching the exposure names. These exposures will already by decorated
40
- # by their Parts.
41
- # - To receive the view's input arguments (whatever is passed to
42
- # `View#call`), provide matching keyword parameters. You can provide
43
- # default values for these parameters to make the corresponding input
44
- # keys optional
45
- # - To receive the Context object, provide a `context:` keyword parameter
46
- # - To receive the view's input arguments in their entirety, provide a
47
- # keywords splat parameter (i.e. `**input`)
48
- #
49
- # @example Accessing input arguments
50
- # expose :article do |slug:|
51
- # article_repo.find_by_slug(slug)
52
- # end
53
- #
54
- # @example Accessing other exposures
55
- # expose :articles do
56
- # article_repo.listing
57
- # end
58
- #
59
- # expose :featured_articles do |articles|
60
- # articles.select(&:featured?)
61
- # end
62
- #
63
- # @param name [Symbol] name for the exposure
64
- # @macro exposure_options
65
- #
66
- # @overload expose(name, **options)
67
- # Define a value to be passed to the template, provided by an instance
68
- # method matching the name. The method's return value will be decorated by
69
- # a matching Part and passed to the template.
70
- #
71
- # The method's parameters will determine what it is given:
72
- #
73
- # - To receive other exposure values, provide positional parameters
74
- # matching the exposure names. These exposures will already by decorated
75
- # by their Parts.
76
- # - To receive the view's input arguments (whatever is passed to
77
- # `View#call`), provide matching keyword parameters. You can provide
78
- # default values for these parameters to make the corresponding input
79
- # keys optional
80
- # - To receive the Context object, provide a `context:` keyword parameter
81
- # - To receive the view's input arguments in their entirey, provide a
82
- # keywords splat parameter (i.e. `**input`)
83
- #
84
- # @example Accessing input arguments
85
- # expose :article
86
- #
87
- # def article(slug:)
88
- # article_repo.find_by_slug(slug)
89
- # end
90
- #
91
- # @example Accessing other exposures
92
- # expose :articles
93
- # expose :featured_articles
94
- #
95
- # def articles
96
- # article_repo.listing
97
- # end
98
- #
99
- # def featured_articles
100
- # articles.select(&:featured?)
101
- # end
102
- #
103
- # @param name [Symbol] name for the exposure
104
- # @macro exposure_options
105
- #
106
- # @overload expose(name, **options)
107
- # Define a single value to pass through from the input data (when there is
108
- # no instance method matching the `name`). This value will be decorated by
109
- # a matching Part and passed to the template.
110
- #
111
- # @param name [Symbol] name for the exposure
112
- # @macro exposure_options
113
- # @option options [Boolean] :default a default value to provide if there is no matching
114
- # input data
115
- #
116
- # @overload expose(*names, **options)
117
- # Define multiple values to pass through from the input data (when there
118
- # is no instance methods matching their names). These values will be
119
- # decorated by matching Parts and passed through to the template.
120
- #
121
- # The provided options will be applied to all the exposures.
122
- #
123
- # @param names [Symbol] names for the exposures
124
- # @macro exposure_options
125
- # @option options [Boolean] :default a default value to provide if there is no matching
126
- # input data
127
- #
128
- # @see https://dry-rb.org/gems/dry-view/exposures/
129
- #
130
- # @api public
131
- def expose(*names, **options, &block)
132
- if names.length == 1
133
- exposures.add(names.first, block, **options)
134
- else
135
- names.each do |name|
136
- exposures.add(name, **options)
137
- end
138
- end
139
- end
140
-
141
- # @api public
142
- def private_expose(*names, **options, &block)
143
- expose(*names, **options, private: true, &block)
144
- end
145
-
146
- # Returns the defined exposures. These are unbound, since bound exposures
147
- # are only created when initializing a View instance.
148
- #
149
- # @return [Exposures]
150
- # @api private
151
- def exposures
152
- @exposures ||= Exposures.new
153
- end
154
-
155
- # @!endgroup
156
-
157
- # @!group Scope
158
-
159
- # Creates and assigns a scope for the current view.
160
- #
161
- # The newly created scope is useful to add custom logic that is specific
162
- # to the view.
163
- #
164
- # The scope has access to locals, exposures, and inherited scope (if any)
165
- #
166
- # If the view already has an explicit scope the newly created scope will
167
- # inherit from the explicit scope.
168
- #
169
- # There are two cases when this may happen:
170
- # 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
171
- # 2. The scope has been inherited by the view superclass
172
- #
173
- # If the view doesn't have an already existing scope, the newly scope
174
- # will inherit from `Hanami::View::Scope` by default.
175
- #
176
- # However, you can specify any base class for it. This is not
177
- # recommended, unless you know what you're doing.
178
- #
179
- # @param scope [Hanami::View::Scope] the current scope (if any), or the
180
- # default base class will be `Hanami::View::Scope`
181
- # @param block [Proc] the scope logic definition
182
- #
183
- # @api public
184
- #
185
- # @example Basic usage
186
- # class MyView < Hanami::View
187
- # config.scope = MyScope
188
- #
189
- # scope do
190
- # def greeting
191
- # _locals[:message].upcase + "!"
192
- # end
193
- #
194
- # def copyright(time)
195
- # "Copy #{time.year}"
196
- # end
197
- # end
198
- # end
199
- #
200
- # # my_view.html.erb
201
- # # <%= greeting %>
202
- # # <%= copyright(Time.now.utc) %>
203
- #
204
- # MyView.new.(message: "Hello") # => "HELLO!"
205
- #
206
- # @example Inherited scope
207
- # class MyScope < Hanami::View::Scope
208
- # private
209
- #
210
- # def shout(string)
211
- # string.upcase + "!"
212
- # end
213
- # end
214
- #
215
- # class MyView < Hanami::View
216
- # config.scope = MyScope
217
- #
218
- # scope do
219
- # def greeting
220
- # shout(_locals[:message])
221
- # end
222
- #
223
- # def copyright(time)
224
- # "Copy #{time.year}"
225
- # end
226
- # end
227
- # end
228
- #
229
- # # my_view.html.erb
230
- # # <%= greeting %>
231
- # # <%= copyright(Time.now.utc) %>
232
- #
233
- # MyView.new.(message: "Hello") # => "HELLO!"
234
- def scope(base: config.scope || Hanami::View::Scope, &block)
235
- config.scope = Class.new(base, &block)
236
- end
237
-
238
- # @!endgroup
239
-
240
- # @!group Render environment
241
-
242
- # Returns a render environment for the view and the given options. This
243
- # environment isn't chdir'ed into any particular directory.
244
- #
245
- # @param format [Symbol] template format to use (defaults to the `default_format` setting)
246
- # @param context [Context] context object to use (defaults to the `default_context` setting)
247
- #
248
- # @see View.template_env render environment for the view's template
249
- # @see View.layout_env render environment for the view's layout
250
- #
251
- # @return [RenderEnvironment]
252
- # @api public
253
- def render_env(format: config.default_format, context: config.default_context)
254
- RenderEnvironment.prepare(renderer(format), config, context)
255
- end
256
-
257
- # @overload template_env(format: config.default_format, context: config.default_context)
258
- # Returns a render environment for the view and the given options,
259
- # chdir'ed into the view's template directory. This is the environment
260
- # used when rendering the template, and is useful to to fetch
261
- # independently when unit testing Parts and Scopes.
262
- #
263
- # @param format [Symbol] template format to use (defaults to the `default_format` setting)
264
- # @param context [Context] context object to use (defaults to the `default_context` setting)
265
- #
266
- # @return [RenderEnvironment]
267
- # @api public
268
- def template_env(**args)
269
- render_env(**args).chdir(config.template)
270
- end
271
-
272
- # @overload layout_env(format: config.default_format, context: config.default_context)
273
- # Returns a render environment for the view and the given options,
274
- # chdir'ed into the view's layout directory. This is the environment used
275
- # when rendering the view's layout.
276
- #
277
- # @param format [Symbol] template format to use (defaults to the `default_format` setting)
278
- # @param context [Context] context object to use (defaults to the `default_context` setting)
279
- #
280
- # @return [RenderEnvironment] @api public
281
- def layout_env(**args)
282
- render_env(**args).chdir(layout_path)
283
- end
284
-
285
- # Returns renderer for the view and provided format
286
- #
287
- # @api private
288
- def renderer(format)
289
- fetch_or_store(:renderer, config, format) {
290
- Renderer.new(
291
- config.paths,
292
- format: format,
293
- engine_mapping: config.renderer_engine_mapping,
294
- **config.renderer_options
295
- )
296
- }
297
- end
298
-
299
- # @api private
300
- def layout_path
301
- File.join(*[config.layouts_dir, config.layout].compact)
302
- end
303
-
304
- # @!endgroup
305
- end
306
-
307
- module InstanceMethods
308
- # Returns an instance of the view. This binds the defined exposures to the
309
- # view instance.
310
- #
311
- # Subclasses can define their own `#initialize` to accept injected
312
- # dependencies, but must call `super()` to ensure the standard view
313
- # initialization can proceed.
314
- #
315
- # @api public
316
- def initialize
317
- @exposures = self.class.exposures.bind(self)
318
- end
319
-
320
- # The view's configuration
321
- #
322
- # @api private
323
- def config
324
- self.class.config
325
- end
326
-
327
- # The view's bound exposures
328
- #
329
- # @return [Exposures]
330
- # @api private
331
- def exposures
332
- @exposures
333
- end
334
-
335
- # Render the view
336
- #
337
- # @param format [Symbol] template format to use
338
- # @param context [Context] context object to use
339
- # @param input input data for preparing exposure values
340
- #
341
- # @return [Rendered] rendered view object
342
- # @api public
343
- def call(format: config.default_format, context: config.default_context, **input)
344
- ensure_config
345
-
346
- env = self.class.render_env(format: format, context: context)
347
- template_env = self.class.template_env(format: format, context: context)
348
-
349
- locals = locals(template_env, input)
350
- output = env.template(config.template, template_env.scope(config.scope, locals))
351
-
352
- if layout?
353
- layout_env = self.class.layout_env(format: format, context: context)
354
- begin
355
- output = env.template(
356
- self.class.layout_path,
357
- layout_env.scope(config.scope, layout_locals(locals))
358
- ) { output }
359
- rescue TemplateNotFoundError
360
- raise LayoutNotFoundError.new(config.layout, config.paths)
361
- end
362
- end
363
-
364
- Rendered.new(output: output, locals: locals)
365
- end
366
-
367
- private
368
-
369
- # @api private
370
- def ensure_config
371
- raise UndefinedConfigError, :paths unless Array(config.paths).any?
372
- raise UndefinedConfigError, :template unless config.template
373
- end
374
-
375
- # @api private
376
- def locals(render_env, input)
377
- exposures.(context: render_env.context, **input) do |value, exposure|
378
- if exposure.decorate? && value
379
- render_env.part(exposure.name, value, **exposure.options)
380
- else
381
- value
382
- end
383
- end
384
- end
385
-
386
- # @api private
387
- def layout_locals(locals)
388
- locals.each_with_object({}) do |(key, value), layout_locals|
389
- layout_locals[key] = value if exposures[key].for_layout?
390
- end
391
- end
392
-
393
- # @api private
394
- def layout?
395
- !!config.layout # rubocop:disable Style/DoubleNegation
396
- end
397
- end
398
- end
399
- end
400
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hanami
4
- class View
5
- module Tilt
6
- module ERB
7
- def self.requirements
8
- ["hanami/view/tilt/erbse", <<~ERROR]
9
- hanami-view requires erbse for full compatibility when rendering .erb templates (e.g. implicitly capturing block content when yielding)
10
-
11
- To ignore this and use another engine for .erb templates, deregister this adapter before calling your views:
12
-
13
- Hanami::View::Tilt.deregister_adapter(:erb)
14
- ERROR
15
- end
16
-
17
- def self.activate
18
- Tilt.default_mapping.register ErbseTemplate, "erb"
19
- self
20
- end
21
- end
22
-
23
- register_adapter :erb, ERB
24
- end
25
- end
26
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tilt/template"
4
- require "erbse"
5
-
6
- module Hanami
7
- class View
8
- module Tilt
9
- # Tilt template class copied from cells-erb gem
10
- class ErbseTemplate < ::Tilt::Template
11
- def prepare
12
- @template = ::Erbse::Engine.new
13
- end
14
-
15
- def precompiled_template(_locals)
16
- @template.call(data)
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hanami
4
- class View
5
- module Tilt
6
- module Haml
7
- def self.requirements
8
- ["hamlit/block", <<~ERROR]
9
- hanami-view requires hamlit-block for full compatibility when rendering .haml templates (e.g. implicitly capturing block content when yielding)
10
-
11
- To ignore this and use another engine for .haml templates, dereigster this adapter before calling your views:
12
-
13
- Hanami::View::Tilt.deregister_adatper(:haml)
14
- ERROR
15
- end
16
-
17
- def self.activate
18
- # Requiring hamlit/block will register the engine with Tilt
19
- self
20
- end
21
- end
22
-
23
- register_adapter :haml, Haml
24
- end
25
- end
26
- end