hanami-view 1.3.3 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -10
  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 +114 -70
  33. data/LICENSE.md +0 -22
  34. data/lib/hanami/layout.rb +0 -190
  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 -281
  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/null_view.rb +0 -26
  48. data/lib/hanami/view/rendering/options.rb +0 -24
  49. data/lib/hanami/view/rendering/partial.rb +0 -31
  50. data/lib/hanami/view/rendering/partial_file.rb +0 -29
  51. data/lib/hanami/view/rendering/partial_finder.rb +0 -75
  52. data/lib/hanami/view/rendering/partial_templates_finder.rb +0 -73
  53. data/lib/hanami/view/rendering/registry.rb +0 -134
  54. data/lib/hanami/view/rendering/scope.rb +0 -108
  55. data/lib/hanami/view/rendering/subscope.rb +0 -56
  56. data/lib/hanami/view/rendering/template.rb +0 -69
  57. data/lib/hanami/view/rendering/template_finder.rb +0 -55
  58. data/lib/hanami/view/rendering/template_name.rb +0 -50
  59. data/lib/hanami/view/rendering/templates_finder.rb +0 -144
  60. data/lib/hanami/view/rendering/view_finder.rb +0 -37
  61. 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