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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -10
- data/LICENSE +20 -0
- data/README.md +20 -862
- data/hanami-view.gemspec +26 -16
- data/lib/hanami-view.rb +3 -1
- data/lib/hanami/view.rb +208 -223
- data/lib/hanami/view/application_configuration.rb +77 -0
- data/lib/hanami/view/application_context.rb +35 -0
- data/lib/hanami/view/application_view.rb +89 -0
- data/lib/hanami/view/context.rb +97 -0
- data/lib/hanami/view/context_helpers/content_helpers.rb +26 -0
- data/lib/hanami/view/decorated_attributes.rb +82 -0
- data/lib/hanami/view/errors.rb +19 -56
- data/lib/hanami/view/exposure.rb +126 -0
- data/lib/hanami/view/exposures.rb +74 -0
- data/lib/hanami/view/part.rb +217 -0
- data/lib/hanami/view/part_builder.rb +140 -0
- data/lib/hanami/view/path.rb +68 -0
- data/lib/hanami/view/render_environment.rb +62 -0
- data/lib/hanami/view/render_environment_missing.rb +44 -0
- data/lib/hanami/view/rendered.rb +55 -0
- data/lib/hanami/view/renderer.rb +79 -0
- data/lib/hanami/view/scope.rb +189 -0
- data/lib/hanami/view/scope_builder.rb +98 -0
- data/lib/hanami/view/standalone_view.rb +396 -0
- data/lib/hanami/view/tilt.rb +78 -0
- data/lib/hanami/view/tilt/erb.rb +26 -0
- data/lib/hanami/view/tilt/erbse.rb +21 -0
- data/lib/hanami/view/tilt/haml.rb +26 -0
- data/lib/hanami/view/version.rb +5 -5
- metadata +114 -70
- data/LICENSE.md +0 -22
- data/lib/hanami/layout.rb +0 -190
- data/lib/hanami/presenter.rb +0 -98
- data/lib/hanami/view/configuration.rb +0 -504
- data/lib/hanami/view/dsl.rb +0 -347
- data/lib/hanami/view/escape.rb +0 -225
- data/lib/hanami/view/inheritable.rb +0 -54
- data/lib/hanami/view/rendering.rb +0 -294
- data/lib/hanami/view/rendering/layout_finder.rb +0 -128
- data/lib/hanami/view/rendering/layout_registry.rb +0 -69
- data/lib/hanami/view/rendering/layout_scope.rb +0 -281
- data/lib/hanami/view/rendering/null_layout.rb +0 -52
- data/lib/hanami/view/rendering/null_local.rb +0 -82
- data/lib/hanami/view/rendering/null_template.rb +0 -83
- data/lib/hanami/view/rendering/null_view.rb +0 -26
- data/lib/hanami/view/rendering/options.rb +0 -24
- data/lib/hanami/view/rendering/partial.rb +0 -31
- data/lib/hanami/view/rendering/partial_file.rb +0 -29
- data/lib/hanami/view/rendering/partial_finder.rb +0 -75
- data/lib/hanami/view/rendering/partial_templates_finder.rb +0 -73
- data/lib/hanami/view/rendering/registry.rb +0 -134
- data/lib/hanami/view/rendering/scope.rb +0 -108
- data/lib/hanami/view/rendering/subscope.rb +0 -56
- data/lib/hanami/view/rendering/template.rb +0 -69
- data/lib/hanami/view/rendering/template_finder.rb +0 -55
- data/lib/hanami/view/rendering/template_name.rb +0 -50
- data/lib/hanami/view/rendering/templates_finder.rb +0 -144
- data/lib/hanami/view/rendering/view_finder.rb +0 -37
- 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
|