hanami-view 1.3.0.beta1 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
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,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