hanami-view 1.3.3 → 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 (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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dry/core/cache"
5
+
6
+ module Hanami
7
+ class View
8
+ # @api private
9
+ class Path
10
+ extend Dry::Core::Cache
11
+ include Dry::Equalizer(:dir, :root)
12
+
13
+ attr_reader :dir, :root
14
+
15
+ def self.[](path)
16
+ if path.is_a?(self)
17
+ path
18
+ else
19
+ new(path)
20
+ end
21
+ end
22
+
23
+ def initialize(dir, root: dir)
24
+ @dir = Pathname(dir)
25
+ @root = Pathname(root)
26
+ end
27
+
28
+ def lookup(name, format, child_dirs: [], parent_dir: false)
29
+ fetch_or_store(dir, root, name, format, child_dirs, parent_dir) do
30
+ lookup_template(name, format) ||
31
+ lookup_in_child_dirs(name, format, child_dirs: child_dirs) ||
32
+ parent_dir && lookup_in_parent_dir(name, format, child_dirs: child_dirs)
33
+ end
34
+ end
35
+
36
+ def chdir(dirname)
37
+ self.class.new(dir.join(dirname), root: root)
38
+ end
39
+
40
+ def to_s
41
+ dir.to_s
42
+ end
43
+
44
+ private
45
+
46
+ def root?
47
+ dir == root
48
+ end
49
+
50
+ # Search for a template using a wildcard for the engine extension
51
+ def lookup_template(name, format)
52
+ glob = dir.join("#{name}.#{format}.*")
53
+ Dir[glob].first
54
+ end
55
+
56
+ def lookup_in_child_dirs(name, format, child_dirs:)
57
+ child_dirs.reduce(nil) { |_, dir|
58
+ template = chdir(dir).lookup(name, format)
59
+ break template if template
60
+ }
61
+ end
62
+
63
+ def lookup_in_parent_dir(name, format, child_dirs:)
64
+ !root? && chdir("..").lookup(name, format, child_dirs: child_dirs, parent_dir: true)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Hanami
6
+ class View
7
+ # @api private
8
+ class RenderEnvironment
9
+ def self.prepare(renderer, config, context)
10
+ new(
11
+ renderer: renderer,
12
+ inflector: config.inflector,
13
+ context: context,
14
+ scope_builder: config.scope_builder.new(namespace: config.scope_namespace),
15
+ part_builder: config.part_builder.new(namespace: config.part_namespace)
16
+ )
17
+ end
18
+
19
+ include Dry::Equalizer(:renderer, :inflector, :context, :scope_builder, :part_builder)
20
+
21
+ attr_reader :renderer, :inflector, :context, :scope_builder, :part_builder
22
+
23
+ def initialize(renderer:, inflector:, context:, scope_builder:, part_builder:)
24
+ @renderer = renderer
25
+ @inflector = inflector
26
+ @context = context.for_render_env(self)
27
+ @scope_builder = scope_builder.for_render_env(self)
28
+ @part_builder = part_builder.for_render_env(self)
29
+ end
30
+
31
+ def format
32
+ renderer.format
33
+ end
34
+
35
+ def part(name, value, **options)
36
+ part_builder.(name, value, **options)
37
+ end
38
+
39
+ def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
40
+ scope_builder.(name, locals)
41
+ end
42
+
43
+ def template(name, scope, &block)
44
+ renderer.template(name, scope, &block)
45
+ end
46
+
47
+ def partial(name, scope, &block)
48
+ renderer.partial(name, scope, &block)
49
+ end
50
+
51
+ def chdir(dirname)
52
+ self.class.new(
53
+ renderer: renderer.chdir(dirname),
54
+ inflector: inflector,
55
+ context: context,
56
+ scope_builder: scope_builder,
57
+ part_builder: part_builder
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -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