hanami-view 1.3.0.beta1 → 2.0.0.alpha2

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE +20 -0
  4. data/README.md +20 -862
  5. data/hanami-view.gemspec +26 -16
  6. data/lib/hanami-view.rb +3 -1
  7. data/lib/hanami/view.rb +208 -223
  8. data/lib/hanami/view/application_configuration.rb +77 -0
  9. data/lib/hanami/view/application_context.rb +35 -0
  10. data/lib/hanami/view/application_view.rb +89 -0
  11. data/lib/hanami/view/context.rb +97 -0
  12. data/lib/hanami/view/context_helpers/content_helpers.rb +26 -0
  13. data/lib/hanami/view/decorated_attributes.rb +82 -0
  14. data/lib/hanami/view/errors.rb +19 -56
  15. data/lib/hanami/view/exposure.rb +126 -0
  16. data/lib/hanami/view/exposures.rb +74 -0
  17. data/lib/hanami/view/part.rb +217 -0
  18. data/lib/hanami/view/part_builder.rb +140 -0
  19. data/lib/hanami/view/path.rb +68 -0
  20. data/lib/hanami/view/render_environment.rb +62 -0
  21. data/lib/hanami/view/render_environment_missing.rb +44 -0
  22. data/lib/hanami/view/rendered.rb +55 -0
  23. data/lib/hanami/view/renderer.rb +79 -0
  24. data/lib/hanami/view/scope.rb +189 -0
  25. data/lib/hanami/view/scope_builder.rb +98 -0
  26. data/lib/hanami/view/standalone_view.rb +396 -0
  27. data/lib/hanami/view/tilt.rb +78 -0
  28. data/lib/hanami/view/tilt/erb.rb +26 -0
  29. data/lib/hanami/view/tilt/erbse.rb +21 -0
  30. data/lib/hanami/view/tilt/haml.rb +26 -0
  31. data/lib/hanami/view/version.rb +5 -5
  32. metadata +113 -63
  33. data/LICENSE.md +0 -22
  34. data/lib/hanami/layout.rb +0 -172
  35. data/lib/hanami/presenter.rb +0 -98
  36. data/lib/hanami/view/configuration.rb +0 -504
  37. data/lib/hanami/view/dsl.rb +0 -347
  38. data/lib/hanami/view/escape.rb +0 -225
  39. data/lib/hanami/view/inheritable.rb +0 -54
  40. data/lib/hanami/view/rendering.rb +0 -294
  41. data/lib/hanami/view/rendering/layout_finder.rb +0 -128
  42. data/lib/hanami/view/rendering/layout_registry.rb +0 -69
  43. data/lib/hanami/view/rendering/layout_scope.rb +0 -274
  44. data/lib/hanami/view/rendering/null_layout.rb +0 -52
  45. data/lib/hanami/view/rendering/null_local.rb +0 -82
  46. data/lib/hanami/view/rendering/null_template.rb +0 -83
  47. data/lib/hanami/view/rendering/options.rb +0 -24
  48. data/lib/hanami/view/rendering/partial.rb +0 -31
  49. data/lib/hanami/view/rendering/partial_file.rb +0 -29
  50. data/lib/hanami/view/rendering/partial_finder.rb +0 -75
  51. data/lib/hanami/view/rendering/partial_templates_finder.rb +0 -73
  52. data/lib/hanami/view/rendering/registry.rb +0 -134
  53. data/lib/hanami/view/rendering/scope.rb +0 -108
  54. data/lib/hanami/view/rendering/subscope.rb +0 -56
  55. data/lib/hanami/view/rendering/template.rb +0 -69
  56. data/lib/hanami/view/rendering/template_finder.rb +0 -55
  57. data/lib/hanami/view/rendering/template_name.rb +0 -50
  58. data/lib/hanami/view/rendering/templates_finder.rb +0 -144
  59. data/lib/hanami/view/rendering/view_finder.rb +0 -37
  60. data/lib/hanami/view/template.rb +0 -57
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require_relative "../view"
5
+
6
+ module Hanami
7
+ class View
8
+ class ApplicationConfiguration
9
+ include Dry::Configurable
10
+
11
+ setting :parts_path, "views/parts"
12
+
13
+ def initialize(*)
14
+ super
15
+
16
+ @base_config = View.config.dup
17
+
18
+ configure_defaults
19
+ end
20
+
21
+ # Returns the list of available settings
22
+ #
23
+ # @return [Set]
24
+ #
25
+ # @since 2.0.0
26
+ # @api private
27
+ def settings
28
+ self.class.settings + View.settings - NON_FORWARDABLE_METHODS
29
+ end
30
+
31
+ def finalize!
32
+ return self if frozen?
33
+
34
+ base_config.finalize!
35
+
36
+ super
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :base_config
42
+
43
+ def configure_defaults
44
+ self.paths = ["web/templates"]
45
+ self.template_inference_base = "views"
46
+ self.layout = "application"
47
+ end
48
+
49
+ # An inflector for views is not configurable via `config.views.inflector` on an
50
+ # `Hanami::Application`. The application-wide inflector is already configurable
51
+ # there as `config.inflector` and will be used as the default inflector for views.
52
+ #
53
+ # A custom inflector may still be provided in an `Hanami::View` subclass, via
54
+ # `config.inflector=`.
55
+ NON_FORWARDABLE_METHODS = [:inflector, :inflector=].freeze
56
+ private_constant :NON_FORWARDABLE_METHODS
57
+
58
+ def method_missing(name, *args, &block)
59
+ return super if NON_FORWARDABLE_METHODS.include?(name)
60
+
61
+ if config.respond_to?(name)
62
+ config.public_send(name, *args, &block)
63
+ elsif base_config.respond_to?(name)
64
+ base_config.public_send(name, *args, &block)
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def respond_to_missing?(name, _include_all = false)
71
+ return false if NON_FORWARDABLE_METHODS.include?(name)
72
+
73
+ config.respond_to?(name) || base_config.respond_to?(name) || super
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class View
5
+ module ApplicationContext
6
+ def initialize(inflector: Hanami.application.inflector, **options)
7
+ @inflector = inflector
8
+ super
9
+ end
10
+
11
+ def inflector
12
+ @inflector
13
+ end
14
+
15
+ def request
16
+ _options.fetch(:request)
17
+ end
18
+
19
+ def session
20
+ request.session
21
+ end
22
+
23
+ def flash
24
+ response.flash
25
+ end
26
+
27
+ private
28
+
29
+ # TODO: create `Request#flash` so we no longer need the `response`
30
+ def response
31
+ _options.fetch(:response)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ module Hanami
2
+ class View
3
+ class ApplicationView < Module
4
+ InheritedHook = Class.new(Module)
5
+
6
+ attr_reader :provider
7
+ attr_reader :application
8
+ attr_reader :inherited_hook
9
+
10
+ def initialize(provider)
11
+ @provider = provider
12
+ @application = provider.respond_to?(:application) ? provider.application : Hanami.application
13
+ @inherited_hook = InheritedHook.new
14
+
15
+ define_inherited_hook
16
+ end
17
+
18
+ def included(view_class)
19
+ configure_view view_class
20
+ view_class.extend inherited_hook
21
+ end
22
+
23
+ private
24
+
25
+ def configure_view(view_class)
26
+ view_class.settings.each do |setting|
27
+ if application.config.views.respond_to?(:"#{setting}")
28
+ application_value = application.config.views.public_send(:"#{setting}")
29
+ view_class.config.public_send :"#{setting}=", application_value
30
+ end
31
+ end
32
+
33
+ view_class.config.inflector = provider.inflector
34
+ view_class.config.paths = prepare_paths(provider, view_class.config.paths)
35
+ view_class.config.template = template_name(view_class)
36
+
37
+ if (part_namespace = namespace_from_path(application.config.views.parts_path))
38
+ view_class.config.part_namespace = part_namespace
39
+ end
40
+ end
41
+
42
+ def define_inherited_hook
43
+ template_name = method(:template_name)
44
+
45
+ inherited_hook.send :define_method, :inherited do |subclass|
46
+ super(subclass)
47
+ subclass.config.template = template_name.(subclass)
48
+ end
49
+ end
50
+
51
+ def prepare_paths(provider, configured_paths)
52
+ configured_paths.map { |path|
53
+ if path.dir.relative?
54
+ provider.root.join(path.dir)
55
+ else
56
+ path
57
+ end
58
+ }
59
+ end
60
+
61
+ def template_name(view_class)
62
+ provider
63
+ .inflector
64
+ .underscore(view_class.name)
65
+ .sub(/^#{provider.namespace_path}\//, "")
66
+ .sub(/^#{view_class.config.template_inference_base}\//, "")
67
+ end
68
+
69
+ def namespace_from_path(path)
70
+ path = "#{provider.namespace_path}/#{path}"
71
+
72
+ begin
73
+ require path
74
+ rescue LoadError => exception
75
+ raise exception unless exception.path == path
76
+ end
77
+
78
+ begin
79
+ inflector.constantize(inflector.camelize(path))
80
+ rescue NameError => exception
81
+ end
82
+ end
83
+
84
+ def inflector
85
+ provider.inflector
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require_relative "application_context"
5
+ require_relative "decorated_attributes"
6
+
7
+ module Hanami
8
+ class View
9
+ # Provides a baseline environment across all the templates, parts and scopes
10
+ # in a given rendering.
11
+ #
12
+ # @abstract Subclass this and add your own methods (along with a custom
13
+ # `#initialize` if you wish to inject dependencies)
14
+ #
15
+ # @api public
16
+ class Context
17
+ include Dry::Equalizer(:_options)
18
+ include DecoratedAttributes
19
+
20
+ attr_reader :_render_env, :_options
21
+
22
+ def self.inherited(subclass)
23
+ super
24
+
25
+ # When inheriting within an Hanami app, add application context behavior
26
+ if application_provider(subclass)
27
+ subclass.include ApplicationContext
28
+ end
29
+ end
30
+
31
+ def self.application_provider(subclass)
32
+ if Hanami.respond_to?(:application?) && Hanami.application?
33
+ Hanami.application.component_provider(subclass)
34
+ end
35
+ end
36
+ private_class_method :application_provider
37
+
38
+ # Returns a new instance of Context
39
+ #
40
+ # In subclasses, you should include an `**options` parameter and pass _all
41
+ # arguments_ to `super`. This allows Context to make copies of itself
42
+ # while preserving your dependencies.
43
+ #
44
+ # @example
45
+ # class MyContext < Hanami::View::Context
46
+ # # Injected dependency
47
+ # attr_reader :assets
48
+ #
49
+ # def initialize(assets:, **options)
50
+ # @assets = assets
51
+ # super
52
+ # end
53
+ # end
54
+ #
55
+ # @api public
56
+ def initialize(render_env: nil, **options)
57
+ @_render_env = render_env
58
+ @_options = options
59
+ end
60
+
61
+ # @api private
62
+ def for_render_env(render_env)
63
+ return self if render_env == _render_env
64
+
65
+ self.class.new(**_options.merge(render_env: render_env))
66
+ end
67
+
68
+ # Returns a copy of the Context with new options merged in.
69
+ #
70
+ # This may be useful to supply values for dependencies that are _optional_
71
+ # when initializing your custom Context subclass.
72
+ #
73
+ # @example
74
+ # class MyContext < Hanami::View::Context
75
+ # # Injected dependencies (request is optional)
76
+ # attr_reader :assets, :request
77
+ #
78
+ # def initialize(assets:, request: nil, **options)
79
+ # @assets = assets
80
+ # @request = reuqest
81
+ # super
82
+ # end
83
+ # end
84
+ #
85
+ # my_context = MyContext.new(assets: assets)
86
+ # my_context_with_request = my_context.with(request: request)
87
+ #
88
+ # @api public
89
+ def with(**new_options)
90
+ self.class.new(
91
+ render_env: _render_env,
92
+ **_options.merge(new_options)
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,26 @@
1
+ module Hanami
2
+ class View
3
+ module ContextHelpers
4
+ module ContentHelpers
5
+ def initialize(content: {}, **options)
6
+ super
7
+ end
8
+
9
+ def content_for(key, value = nil, &block)
10
+ content = _options[:content]
11
+ output = nil
12
+
13
+ if block
14
+ content[key] = yield
15
+ elsif value
16
+ content[key] = value
17
+ else
18
+ output = content[key]
19
+ end
20
+
21
+ output
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Hanami
6
+ class View
7
+ # Decorates attributes in Parts.
8
+ module DecoratedAttributes
9
+ # @api private
10
+ def self.included(klass)
11
+ klass.extend ClassInterface
12
+ end
13
+
14
+ # Decorated attributes class-level interface.
15
+ module ClassInterface
16
+ # @api private
17
+ MODULE_NAME = :DecoratedAttributes
18
+
19
+ # Decorates the provided attributes, wrapping them in Parts using the
20
+ # current render environment.
21
+ #
22
+ # @example
23
+ # class Article < Hanami::View::Part
24
+ # decorate :feature_image
25
+ # decorate :author as: :person
26
+ # end
27
+ #
28
+ # @param names [Array<Symbol>] the attribute names
29
+ # @param options [Hash] the options to pass to the Part Builder
30
+ # @option options [Symbol, Class] :as an alternative name or class to use when finding a
31
+ # matching Part
32
+ #
33
+ # @api public
34
+ def decorate(*names, **options)
35
+ decorated_attributes.decorate(*names, **options)
36
+ end
37
+
38
+ private
39
+
40
+ def decorated_attributes
41
+ if const_defined?(MODULE_NAME, false)
42
+ const_get(MODULE_NAME)
43
+ else
44
+ const_set(MODULE_NAME, Attributes.new).tap do |mod|
45
+ prepend mod
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ class Attributes < Module
53
+ def initialize(*)
54
+ @names = Set.new
55
+ super
56
+ end
57
+
58
+ def decorate(*names, **options)
59
+ @names += names
60
+
61
+ class_eval do
62
+ names.each do |name|
63
+ define_method name do
64
+ attribute = super()
65
+
66
+ if _render_env && attribute
67
+ _render_env.part(name, attribute, **options)
68
+ else
69
+ attribute
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def inspect
77
+ %(#<#{self.class.name}#{@names.to_a.sort.inspect}>)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,65 +1,28 @@
1
- module Hanami
2
- module View
3
- # @since 0.5.0
4
- class Error < ::StandardError
5
- end
1
+ # frozen_string_literal: true
6
2
 
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.
3
+ module Hanami
4
+ class View
5
+ # Error raised when critical settings are not configured
17
6
  #
18
- # @since 0.1.0
19
- class MissingTemplateError < Error
20
- # @since 0.1.0
21
- # @api private
22
- def initialize(template, format)
23
- super("Can't find template '#{ template }' for '#{ format }' format.")
7
+ # @api private
8
+ class UndefinedConfigError < StandardError
9
+ def initialize(key)
10
+ super("no +#{key}+ configured")
24
11
  end
25
12
  end
26
13
 
27
- # Missing format error
28
- #
29
- # This is raised at the runtime when rendering context lacks the :format
30
- # key.
31
- #
32
- # @since 0.1.0
33
- #
34
- # @see Hanami::View::Rendering#render
35
- class MissingFormatError < Error
36
- end
37
-
38
- # Missing template layout error
39
- #
40
- # This is raised at the runtime when Hanami::Layout cannot find its template.
41
- #
42
- # @since 0.5.0
43
- class MissingTemplateLayoutError < Error
44
- # @since 0.5.0
45
- # @api private
46
- def initialize(template)
47
- super("Can't find layout template '#{ template }'")
48
- end
49
- end
14
+ # Error raised when template could not be found within a view's configured
15
+ # paths
16
+ #
17
+ # @api private
18
+ class TemplateNotFoundError < StandardError
19
+ def initialize(template_name, lookup_paths)
20
+ msg = [
21
+ "Template +#{template_name}+ could not be found in paths:",
22
+ lookup_paths.map { |path| " - #{path}" }
23
+ ].join("\n\n")
50
24
 
51
- # Unknown or render type
52
- #
53
- # This is raised at the runtime when `Hanami::Layout` doesn't recognize the render type.
54
- #
55
- # @since 1.1.0
56
- class UnknownRenderTypeError < Error
57
- # @since 1.1.0
58
- # @api private
59
- def initialize(known_types, supplied_options)
60
- known_types_list = known_types.map{|t| "':#{t}'"}.join(', ')
61
- supplied_options_list = supplied_options.keys.map{|t| "':#{t}'"}.join(', ')
62
- super("Calls to `render` in a layout must include one of #{known_types_list}. Found #{supplied_options_list}.")
25
+ super(msg)
63
26
  end
64
27
  end
65
28
  end