hanami-view 1.3.1 → 2.0.0.alpha3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +20 -0
  4. data/README.md +17 -835
  5. data/hanami-view.gemspec +26 -16
  6. data/lib/hanami/view/application_configuration.rb +77 -0
  7. data/lib/hanami/view/application_context.rb +35 -0
  8. data/lib/hanami/view/application_view.rb +89 -0
  9. data/lib/hanami/view/context.rb +97 -0
  10. data/lib/hanami/view/context_helpers/content_helpers.rb +26 -0
  11. data/lib/hanami/view/decorated_attributes.rb +82 -0
  12. data/lib/hanami/view/errors.rb +31 -53
  13. data/lib/hanami/view/exposure.rb +126 -0
  14. data/lib/hanami/view/exposures.rb +74 -0
  15. data/lib/hanami/view/part.rb +217 -0
  16. data/lib/hanami/view/part_builder.rb +140 -0
  17. data/lib/hanami/view/path.rb +68 -0
  18. data/lib/hanami/view/render_environment.rb +62 -0
  19. data/lib/hanami/view/render_environment_missing.rb +44 -0
  20. data/lib/hanami/view/rendered.rb +55 -0
  21. data/lib/hanami/view/renderer.rb +79 -0
  22. data/lib/hanami/view/scope.rb +189 -0
  23. data/lib/hanami/view/scope_builder.rb +98 -0
  24. data/lib/hanami/view/standalone_view.rb +400 -0
  25. data/lib/hanami/view/tilt/erb.rb +26 -0
  26. data/lib/hanami/view/tilt/erbse.rb +21 -0
  27. data/lib/hanami/view/tilt/haml.rb +26 -0
  28. data/lib/hanami/view/tilt.rb +78 -0
  29. data/lib/hanami/view/version.rb +5 -5
  30. data/lib/hanami/view.rb +208 -223
  31. data/lib/hanami-view.rb +3 -1
  32. metadata +120 -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/layout_finder.rb +0 -128
  41. data/lib/hanami/view/rendering/layout_registry.rb +0 -69
  42. data/lib/hanami/view/rendering/layout_scope.rb +0 -274
  43. data/lib/hanami/view/rendering/null_layout.rb +0 -52
  44. data/lib/hanami/view/rendering/null_local.rb +0 -82
  45. data/lib/hanami/view/rendering/null_template.rb +0 -83
  46. data/lib/hanami/view/rendering/null_view.rb +0 -26
  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/rendering.rb +0 -294
  61. data/lib/hanami/view/template.rb +0 -57
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/cache"
4
+ require "dry/core/equalizer"
5
+ require_relative "errors"
6
+ require_relative "tilt"
7
+
8
+ module Hanami
9
+ class View
10
+ # @api private
11
+ class Renderer
12
+ PARTIAL_PREFIX = "_"
13
+ PATH_DELIMITER = "/"
14
+
15
+ extend Dry::Core::Cache
16
+
17
+ include Dry::Equalizer(:paths, :format, :engine_mapping, :options)
18
+
19
+ attr_reader :paths, :format, :engine_mapping, :options
20
+
21
+ def initialize(paths, format:, engine_mapping: nil, **options)
22
+ @paths = paths
23
+ @format = format
24
+ @engine_mapping = engine_mapping || {}
25
+ @options = options
26
+ end
27
+
28
+ def template(name, scope, **lookup_options, &block)
29
+ path = lookup(name, **lookup_options)
30
+
31
+ if path
32
+ render(path, scope, &block)
33
+ else
34
+ raise TemplateNotFoundError.new(name, paths)
35
+ end
36
+ end
37
+
38
+ def partial(name, scope, &block)
39
+ template(
40
+ name_for_partial(name),
41
+ scope,
42
+ child_dirs: %w[shared],
43
+ parent_dir: true,
44
+ &block
45
+ )
46
+ end
47
+
48
+ def render(path, scope, &block)
49
+ tilt(path).render(scope, &block)
50
+ end
51
+
52
+ def chdir(dirname)
53
+ new_paths = paths.map { |path| path.chdir(dirname) }
54
+
55
+ self.class.new(new_paths, format: format, **options)
56
+ end
57
+
58
+ private
59
+
60
+ def lookup(name, **options)
61
+ paths.inject(nil) { |_, path|
62
+ result = path.lookup(name, format, **options)
63
+ break result if result
64
+ }
65
+ end
66
+
67
+ def name_for_partial(name)
68
+ name_segments = name.to_s.split(PATH_DELIMITER)
69
+ name_segments[0..-2].push("#{PARTIAL_PREFIX}#{name_segments[-1]}").join(PATH_DELIMITER)
70
+ end
71
+
72
+ def tilt(path)
73
+ fetch_or_store(:engine, path, engine_mapping, options) {
74
+ Tilt[path, engine_mapping, **options]
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/core/constants"
5
+ require_relative "render_environment_missing"
6
+
7
+ module Hanami
8
+ class View
9
+ # Evaluation context for templates (including layouts and partials) and
10
+ # provides a place to encapsulate view-specific behaviour alongside a
11
+ # template and its locals.
12
+ #
13
+ # @abstract Subclass this and provide your own methods adding view-specific
14
+ # behavior. You should not override `#initialize`
15
+ #
16
+ # @see https://dry-rb.org/gems/dry-view/templates/
17
+ # @see https://dry-rb.org/gems/dry-view/scopes/
18
+ #
19
+ # @api public
20
+ class Scope
21
+ # @api private
22
+ CONVENIENCE_METHODS = %i[format context locals].freeze
23
+
24
+ include Dry::Equalizer(:_name, :_locals, :_render_env)
25
+
26
+ # The scope's name
27
+ #
28
+ # @return [Symbol]
29
+ #
30
+ # @api public
31
+ attr_reader :_name
32
+
33
+ # The scope's locals
34
+ #
35
+ # @overload _locals
36
+ # Returns the locals
37
+ # @overload locals
38
+ # A convenience alias for `#_format.` Is available unless there is a
39
+ # local named `locals`
40
+ #
41
+ # @return [Hash[<Symbol, Object>]
42
+ #
43
+ # @api public
44
+ attr_reader :_locals
45
+
46
+ # The current render environment
47
+ #
48
+ # @return [RenderEnvironment] render environment
49
+ #
50
+ # @api private
51
+ attr_reader :_render_env
52
+
53
+ # Returns a new Scope instance
54
+ #
55
+ # @param name [Symbol, nil] scope name
56
+ # @param locals [Hash<Symbol, Object>] template locals
57
+ # @param render_env [RenderEnvironment] render environment
58
+ #
59
+ # @return [Scope]
60
+ #
61
+ # @api public
62
+ def initialize(
63
+ name: nil,
64
+ locals: Dry::Core::Constants::EMPTY_HASH,
65
+ render_env: RenderEnvironmentMissing.new
66
+ )
67
+ @_name = name
68
+ @_locals = locals
69
+ @_render_env = render_env
70
+ end
71
+
72
+ # @overload render(partial_name, **locals, &block)
73
+ # Renders a partial using the scope
74
+ #
75
+ # @param partial_name [Symbol, String] partial name
76
+ # @param locals [Hash<Symbol, Object>] partial locals
77
+ # @yieldreturn [String] string content to include where the partial calls `yield`
78
+ #
79
+ # @overload render(**locals, &block)
80
+ # Renders a partial (named after the scope's own name) using the scope
81
+ #
82
+ # @param locals[Hash<Symbol, Object>] partial locals
83
+ # @yieldreturn [String] string content to include where the partial calls `yield`
84
+ #
85
+ # @return [String] the rendered partial output
86
+ #
87
+ # @api public
88
+ def render(partial_name = nil, **locals, &block)
89
+ partial_name ||= _name
90
+
91
+ unless partial_name
92
+ raise ArgumentError, "+partial_name+ must be provided for unnamed scopes"
93
+ end
94
+
95
+ if partial_name.is_a?(Class)
96
+ partial_name = _inflector.underscore(_inflector.demodulize(partial_name.to_s))
97
+ end
98
+
99
+ _render_env.partial(partial_name, _render_scope(**locals), &block)
100
+ end
101
+
102
+ # Build a new scope using a scope class matching the provided name
103
+ #
104
+ # @param name [Symbol, Class] scope name (or class)
105
+ # @param locals [Hash<Symbol, Object>] scope locals
106
+ #
107
+ # @return [Scope]
108
+ #
109
+ # @api public
110
+ def scope(name = nil, **locals)
111
+ _render_env.scope(name, locals)
112
+ end
113
+
114
+ # The template format for the current render environment.
115
+ #
116
+ # @overload _format
117
+ # Returns the format.
118
+ # @overload format
119
+ # A convenience alias for `#_format.` Is available unless there is a
120
+ # local named `format`
121
+ #
122
+ # @return [Symbol] format
123
+ #
124
+ # @api public
125
+ def _format
126
+ _render_env.format
127
+ end
128
+
129
+ # The context object for the current render environment
130
+ #
131
+ # @overload _context
132
+ # Returns the context.
133
+ # @overload context
134
+ # A convenience alias for `#_context`. Is available unless there is a
135
+ # local named `context`.
136
+ #
137
+ # @return [Context] context
138
+ #
139
+ # @api public
140
+ def _context
141
+ _render_env.context
142
+ end
143
+
144
+ private
145
+
146
+ # Handles missing methods, according to the following rules:
147
+ #
148
+ # 1. If there is a local with a name matching the method, it returns the
149
+ # local.
150
+ # 2. If the `context` responds to the method, then it will be sent the
151
+ # method and all its arguments.
152
+ def method_missing(name, *args, &block)
153
+ if _locals.key?(name)
154
+ _locals[name]
155
+ elsif _context.respond_to?(name)
156
+ _context.public_send(name, *args, &block)
157
+ elsif CONVENIENCE_METHODS.include?(name)
158
+ __send__(:"_#{name}", *args, &block)
159
+ else
160
+ super
161
+ end
162
+ end
163
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
164
+
165
+ def respond_to_missing?(name, include_private = false)
166
+ _locals.key?(name) ||
167
+ _render_env.context.respond_to?(name) ||
168
+ CONVENIENCE_METHODS.include?(name) ||
169
+ super
170
+ end
171
+
172
+ def _render_scope(**locals)
173
+ if locals.none?
174
+ self
175
+ else
176
+ self.class.new(
177
+ # FIXME: what about `name`?
178
+ locals: locals,
179
+ render_env: _render_env
180
+ )
181
+ end
182
+ end
183
+
184
+ def _inflector
185
+ _render_env.inflector
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/cache"
4
+ require "dry/core/equalizer"
5
+ require_relative "scope"
6
+
7
+ module Hanami
8
+ class View
9
+ # Builds scope objects via matching classes
10
+ #
11
+ # @api private
12
+ class ScopeBuilder
13
+ extend Dry::Core::Cache
14
+ include Dry::Equalizer(:namespace)
15
+
16
+ # The view's configured `scope_namespace`
17
+ #
18
+ # @api private
19
+ attr_reader :namespace
20
+
21
+ # @return [RenderEnvironment]
22
+ #
23
+ # @api private
24
+ attr_reader :render_env
25
+
26
+ # Returns a new instance of ScopeBuilder
27
+ #
28
+ # @api private
29
+ def initialize(namespace: nil, render_env: nil)
30
+ @namespace = namespace
31
+ @render_env = render_env
32
+ end
33
+
34
+ # @api private
35
+ def for_render_env(render_env)
36
+ return self if render_env == self.render_env
37
+
38
+ self.class.new(namespace: namespace, render_env: render_env)
39
+ end
40
+
41
+ # Returns a new scope using a class matching the name
42
+ #
43
+ # @param name [Symbol, Class] scope name
44
+ # @param locals [Hash<Symbol, Object>] locals hash
45
+ #
46
+ # @return [Hanami::View::Scope]
47
+ #
48
+ # @api private
49
+ def call(name = nil, locals) # rubocop:disable Style/OptionalArguments
50
+ scope_class(name).new(
51
+ name: name,
52
+ locals: locals,
53
+ render_env: render_env
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ DEFAULT_SCOPE_CLASS = Scope
60
+
61
+ def scope_class(name = nil)
62
+ if name.nil?
63
+ DEFAULT_SCOPE_CLASS
64
+ elsif name.is_a?(Class)
65
+ name
66
+ else
67
+ fetch_or_store(namespace, name) do
68
+ resolve_scope_class(name: name)
69
+ end
70
+ end
71
+ end
72
+
73
+ def resolve_scope_class(name:)
74
+ name = inflector.camelize(name.to_s)
75
+
76
+ # Give autoloaders a chance to act
77
+ begin
78
+ klass = namespace.const_get(name)
79
+ rescue NameError # rubocop:disable Lint/HandleExceptions
80
+ end
81
+
82
+ if !klass && namespace.const_defined?(name, false)
83
+ klass = namespace.const_get(name)
84
+ end
85
+
86
+ if klass && klass < Scope
87
+ klass
88
+ else
89
+ DEFAULT_SCOPE_CLASS
90
+ end
91
+ end
92
+
93
+ def inflector
94
+ render_env.inflector
95
+ end
96
+ end
97
+ end
98
+ end