hanami-view 1.3.2 → 2.0.0.alpha5

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 +14 -4
  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 +57 -0
  8. data/lib/hanami/view/application_view.rb +89 -0
  9. data/lib/hanami/view/context.rb +98 -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 -267
  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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ module Hanami
6
+ class View
7
+ # @api private
8
+ class RenderEnvironmentMissing
9
+ class MissingEnvironmentError < StandardError
10
+ def message
11
+ "a +render_env+ must be provided"
12
+ end
13
+ end
14
+
15
+ def format
16
+ raise MissingEnvironmentError
17
+ end
18
+
19
+ def context
20
+ raise MissingEnvironmentError
21
+ end
22
+
23
+ def part(_name, _value, **_options)
24
+ raise MissingEnvironmentError
25
+ end
26
+
27
+ def scope(_name = nil, _locals) # rubocop:disable Style/OptionalArguments
28
+ raise MissingEnvironmentError
29
+ end
30
+
31
+ def template(_name, _scope)
32
+ raise MissingEnvironmentError
33
+ end
34
+
35
+ def partial(_name, _scope)
36
+ raise MissingEnvironmentError
37
+ end
38
+
39
+ def inflector
40
+ @inflector ||= Dry::Inflector.new
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Hanami
6
+ class View
7
+ # Output of a View rendering
8
+ #
9
+ # @api public
10
+ class Rendered
11
+ include Dry::Equalizer(:output, :locals)
12
+
13
+ # Returns the rendered view
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @api public
18
+ attr_reader :output
19
+
20
+ # Returns the hash of locals used to render the view
21
+ #
22
+ # @return [Hash[<Symbol, Hanami::View::Part>] locals hash
23
+ #
24
+ # @api public
25
+ attr_reader :locals
26
+
27
+ # @api private
28
+ def initialize(output:, locals:)
29
+ @output = output
30
+ @locals = locals
31
+ end
32
+
33
+ # Returns the local corresponding to the key
34
+ #
35
+ # @param name [Symbol] local key
36
+ #
37
+ # @return [Hanami::View::Part]
38
+ #
39
+ # @api public
40
+ def [](name)
41
+ locals[name]
42
+ end
43
+
44
+ # Returns the rendered view
45
+ #
46
+ # @return [String]
47
+ #
48
+ # @api public
49
+ def to_s
50
+ output
51
+ end
52
+ alias_method :to_str, :to_s
53
+ end
54
+ end
55
+ end
@@ -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