phlex-hanami 0.2.0.pre.alpha.2 → 0.2.0
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 +61 -1
- data/lib/phlex/hanami/component.rb +41 -0
- data/lib/phlex/hanami/context.rb +21 -1
- data/lib/phlex/hanami/contextual.rb +294 -0
- data/lib/phlex/hanami/errors/mailer_view_error.rb +1 -0
- data/lib/phlex/hanami/errors/missing_context_error.rb +1 -0
- data/lib/phlex/hanami/errors/relative_path_error.rb +1 -0
- data/lib/phlex/hanami/extensions/mailer.rb +6 -0
- data/lib/phlex/hanami/extensions/slice.rb +12 -3
- data/lib/phlex/hanami/helpers.rb +3 -2
- data/lib/phlex/hanami/mailer/renderable.rb +6 -0
- data/lib/phlex/hanami/mailer/text.rb +19 -6
- data/lib/phlex/hanami/renderable.rb +14 -237
- data/lib/phlex/hanami/rspec.rb +14 -0
- data/lib/phlex/hanami/slice_configured.rb +5 -1
- data/lib/phlex/hanami/slice_configured_context.rb +9 -1
- data/lib/phlex/hanami/testing.rb +156 -0
- data/lib/phlex/hanami.rb +3 -1
- data/sig/external/dry.rbs +9 -0
- data/sig/external/hanami.rbs +77 -0
- data/sig/external/hanami_view.rbs +9 -0
- data/sig/external/phlex.rbs +16 -0
- data/sig/phlex/hanami/component.rbs +9 -0
- data/sig/phlex/hanami/context.rbs +39 -0
- data/sig/phlex/hanami/contextual.rbs +57 -0
- data/sig/phlex/hanami/errors/error.rbs +8 -0
- data/sig/phlex/hanami/errors/mailer_view_error.rbs +9 -0
- data/sig/phlex/hanami/errors/missing_context_error.rbs +9 -0
- data/sig/phlex/hanami/errors/relative_path_error.rbs +9 -0
- data/sig/phlex/hanami/extensions/mailer.rbs +25 -0
- data/sig/phlex/hanami/extensions/slice.rbs +21 -0
- data/sig/phlex/hanami/helpers.rbs +13 -0
- data/sig/phlex/hanami/layout.rbs +9 -0
- data/sig/phlex/hanami/mailer/layout.rbs +11 -0
- data/sig/phlex/hanami/mailer/renderable.rbs +27 -0
- data/sig/phlex/hanami/mailer/text.rbs +47 -0
- data/sig/phlex/hanami/mailer/view.rbs +11 -0
- data/sig/phlex/hanami/mailer.rbs +8 -0
- data/sig/phlex/hanami/renderable.rbs +27 -0
- data/sig/phlex/hanami/rspec.rbs +1 -0
- data/sig/phlex/hanami/slice_configured.rbs +19 -0
- data/sig/phlex/hanami/slice_configured_context.rbs +27 -0
- data/sig/phlex/hanami/testing.rbs +19 -0
- data/sig/phlex/hanami/view.rbs +9 -0
- data/sig/phlex/hanami.rbs +7 -0
- data/sig/phlex-hanami.rbs +1 -2
- metadata +32 -1
data/lib/phlex/hanami/helpers.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Phlex
|
|
|
46
46
|
OVERRIDDEN_METHODS = {
|
|
47
47
|
raw: Phlex::SGML.instance_method(:raw),
|
|
48
48
|
tag: Phlex::HTML.instance_method(:tag),
|
|
49
|
-
}.freeze
|
|
49
|
+
}.freeze #: Hash[Symbol, UnboundMethod]
|
|
50
50
|
|
|
51
51
|
# Phlex's implementations, mixed back in over Hanami's.
|
|
52
52
|
#
|
|
@@ -56,10 +56,11 @@ module Phlex
|
|
|
56
56
|
OVERRIDDEN_METHODS.each do |name, method|
|
|
57
57
|
define_method(name) { |*args, **options, &block| method.bind_call(self, *args, **options, &block) }
|
|
58
58
|
end
|
|
59
|
-
end
|
|
59
|
+
end #: Module
|
|
60
60
|
|
|
61
61
|
# @api private
|
|
62
62
|
# @since 0.2.0
|
|
63
|
+
#: (singleton(::Phlex::SGML)) -> void
|
|
63
64
|
def self.included(view_class)
|
|
64
65
|
view_class.include(::Hanami::Extensions::View::StandardHelpers)
|
|
65
66
|
view_class.include(PhlexMethods)
|
|
@@ -22,6 +22,7 @@ module Phlex
|
|
|
22
22
|
module Renderable
|
|
23
23
|
# @api private
|
|
24
24
|
# @since 0.2.0
|
|
25
|
+
#: (singleton(::Phlex::SGML)) -> void
|
|
25
26
|
def self.included(view_class)
|
|
26
27
|
view_class.include(::Phlex::Hanami::Renderable)
|
|
27
28
|
|
|
@@ -48,6 +49,7 @@ module Phlex
|
|
|
48
49
|
#
|
|
49
50
|
# @api public
|
|
50
51
|
# @since 0.2.0
|
|
52
|
+
#: (?context: (::Hanami::View::Context | Context)?, ?format: Symbol | String, **untyped) -> String
|
|
51
53
|
def call(context: nil, format: :html, **input)
|
|
52
54
|
html = super(context: context || mail_context, **input)
|
|
53
55
|
return html unless format.to_s == "text"
|
|
@@ -62,6 +64,7 @@ module Phlex
|
|
|
62
64
|
# The web layout is the wrong one for an email — it carries the stylesheets, scripts and
|
|
63
65
|
# page chrome no mail client wants — so mail looks for its own, and renders without a
|
|
64
66
|
# layout when the slice has none.
|
|
67
|
+
#: () -> singleton(::Phlex::SGML)?
|
|
65
68
|
def default_layout
|
|
66
69
|
return nil unless slice
|
|
67
70
|
return nil unless slice.namespace.const_defined?(:Views, false)
|
|
@@ -78,6 +81,7 @@ module Phlex
|
|
|
78
81
|
# The same class an action would be given, so a view reads the same in both places. What
|
|
79
82
|
# a request would have filled in — `request`, `session`, `flash`, `csrf_token` — is
|
|
80
83
|
# missing, and the context says so when a view reaches for it.
|
|
84
|
+
#: () -> (::Hanami::View::Context | Context)?
|
|
81
85
|
def mail_context
|
|
82
86
|
return nil unless slice
|
|
83
87
|
|
|
@@ -99,6 +103,7 @@ module Phlex
|
|
|
99
103
|
#
|
|
100
104
|
# @api public
|
|
101
105
|
# @since 0.2.0
|
|
106
|
+
#: (*untyped, **untyped) -> bot
|
|
102
107
|
def path(*, **)
|
|
103
108
|
raise RelativePathError, self.class
|
|
104
109
|
end
|
|
@@ -119,6 +124,7 @@ module Phlex
|
|
|
119
124
|
#
|
|
120
125
|
# @api public
|
|
121
126
|
# @since 0.2.0
|
|
127
|
+
#: (String) -> String
|
|
122
128
|
def text_body(html)
|
|
123
129
|
Text.call(html)
|
|
124
130
|
end
|
|
@@ -21,11 +21,11 @@ module Phlex
|
|
|
21
21
|
# @since 0.2.0
|
|
22
22
|
BLOCK_ELEMENTS = %w[
|
|
23
23
|
article blockquote div footer h1 h2 h3 h4 h5 h6 header hr li ol p section table td tr ul
|
|
24
|
-
].freeze
|
|
24
|
+
].freeze #: Array[String]
|
|
25
25
|
|
|
26
26
|
# @api private
|
|
27
27
|
# @since 0.2.0
|
|
28
|
-
BLOCK_PATTERN = %r{</?(?:#{BLOCK_ELEMENTS.join('|')})\b[^>]*>}i
|
|
28
|
+
BLOCK_PATTERN = %r{</?(?:#{BLOCK_ELEMENTS.join('|')})\b[^>]*>}i #: Regexp
|
|
29
29
|
|
|
30
30
|
# The entities Phlex escapes, and the characters they stand for.
|
|
31
31
|
#
|
|
@@ -39,22 +39,22 @@ module Phlex
|
|
|
39
39
|
"<" => "<",
|
|
40
40
|
" " => " ",
|
|
41
41
|
""" => '"',
|
|
42
|
-
}.freeze
|
|
42
|
+
}.freeze #: Hash[String, String]
|
|
43
43
|
|
|
44
44
|
# @api private
|
|
45
45
|
# @since 0.2.0
|
|
46
|
-
ENTITY_PATTERN = /&(?:amp|apos|gt|lt|nbsp|quot|#39);/
|
|
46
|
+
ENTITY_PATTERN = /&(?:amp|apos|gt|lt|nbsp|quot|#39);/ #: Regexp
|
|
47
47
|
|
|
48
48
|
# Stand in for the angle brackets around a URL until the tags are gone, so that stripping
|
|
49
49
|
# tags does not eat the link the converter has just written.
|
|
50
50
|
#
|
|
51
51
|
# @api private
|
|
52
52
|
# @since 0.2.0
|
|
53
|
-
LINK_CLOSE = "\u0011"
|
|
53
|
+
LINK_CLOSE = "\u0011" #: String
|
|
54
54
|
|
|
55
55
|
# @api private
|
|
56
56
|
# @since 0.2.0
|
|
57
|
-
LINK_OPEN = "\u0010"
|
|
57
|
+
LINK_OPEN = "\u0010" #: String
|
|
58
58
|
|
|
59
59
|
class << self
|
|
60
60
|
# Converts rendered HTML to plain text.
|
|
@@ -65,6 +65,7 @@ module Phlex
|
|
|
65
65
|
#
|
|
66
66
|
# @api public
|
|
67
67
|
# @since 0.2.0
|
|
68
|
+
#: (_ToS) -> String
|
|
68
69
|
def call(html)
|
|
69
70
|
text = body_of(html.to_s)
|
|
70
71
|
text = strip_hidden_elements(text)
|
|
@@ -80,6 +81,7 @@ module Phlex
|
|
|
80
81
|
private
|
|
81
82
|
|
|
82
83
|
# `Label <https://example.com/path>`, or one of the two when the other adds nothing.
|
|
84
|
+
#: (String, String) -> String
|
|
83
85
|
def anchor_text(attributes, inner)
|
|
84
86
|
url = href_in(attributes)
|
|
85
87
|
label = strip_tags(inner).gsub(/\s+/, " ").strip
|
|
@@ -89,18 +91,24 @@ module Phlex
|
|
|
89
91
|
"#{label} #{LINK_OPEN}#{url}#{LINK_CLOSE}"
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
#: (String) -> String
|
|
92
95
|
def body_of(html) = html[%r{<body\b[^>]*>(.*?)</body>}mi, 1] || html
|
|
93
96
|
|
|
97
|
+
#: (String) -> String
|
|
94
98
|
def expand_anchors(html)
|
|
95
99
|
html.gsub(%r{<a\b([^>]*)>(.*?)</a>}mi) { anchor_text(Regexp.last_match(1), Regexp.last_match(2)) }
|
|
96
100
|
end
|
|
97
101
|
|
|
102
|
+
#: (String) -> String
|
|
98
103
|
def expand_blocks(html) = html.gsub(BLOCK_PATTERN, "\n\n")
|
|
99
104
|
|
|
105
|
+
#: (String) -> String
|
|
100
106
|
def expand_breaks(html) = html.gsub(%r{<br\b[^>]*/?>}i, "\n")
|
|
101
107
|
|
|
108
|
+
#: (String) -> String
|
|
102
109
|
def expand_list_items(html) = html.gsub(%r{</li\s*>}i, "").gsub(/<li\b[^>]*>/i, "\n- ")
|
|
103
110
|
|
|
111
|
+
#: (String) -> String
|
|
104
112
|
def href_in(attributes)
|
|
105
113
|
match = attributes.match(/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i)
|
|
106
114
|
return "" unless match
|
|
@@ -108,14 +116,19 @@ module Phlex
|
|
|
108
116
|
(match[1] || match[2] || match[3]).to_s.strip
|
|
109
117
|
end
|
|
110
118
|
|
|
119
|
+
#: (String) -> String
|
|
111
120
|
def restore_links(text) = text.gsub(LINK_OPEN, "<").gsub(LINK_CLOSE, ">")
|
|
112
121
|
|
|
122
|
+
#: (String) -> String
|
|
113
123
|
def strip_hidden_elements(html) = html.gsub(%r{<(head|style|script)\b[^>]*>.*?</\1\s*>}mi, "")
|
|
114
124
|
|
|
125
|
+
#: (String) -> String
|
|
115
126
|
def strip_tags(html) = html.gsub(/<!--.*?-->/m, "").gsub(/<[^>]*>/, "")
|
|
116
127
|
|
|
128
|
+
#: (String) -> String
|
|
117
129
|
def tidy(text) = text.gsub(/[ \t]+/, " ").gsub(/[ \t]+$/, "").gsub(/\n{3,}/, "\n\n").strip
|
|
118
130
|
|
|
131
|
+
#: (String) -> String
|
|
119
132
|
def unescape(text) = text.gsub(ENTITY_PATTERN) { ENTITIES.fetch(Regexp.last_match(0)) }
|
|
120
133
|
end
|
|
121
134
|
end
|
|
@@ -5,8 +5,11 @@ module Phlex
|
|
|
5
5
|
# Makes a Phlex class renderable by Hanami.
|
|
6
6
|
#
|
|
7
7
|
# Include this into an existing `Phlex::SGML` subclass, or subclass {View}, which includes it
|
|
8
|
-
# already. It gives the class the `call` contract Hanami's `Response#render` expects,
|
|
9
|
-
# configuration
|
|
8
|
+
# already. It gives the class the `call` contract Hanami's `Response#render` expects, a layout,
|
|
9
|
+
# and everything in {Contextual}: per-slice configuration and the request's view context.
|
|
10
|
+
#
|
|
11
|
+
# A component is not called by Hanami and wants no layout, so it includes {Contextual} on its
|
|
12
|
+
# own. {Component} does that already.
|
|
10
13
|
#
|
|
11
14
|
# @example
|
|
12
15
|
# module MyApp
|
|
@@ -22,70 +25,21 @@ module Phlex
|
|
|
22
25
|
#
|
|
23
26
|
# @api private
|
|
24
27
|
# @since 0.2.0
|
|
25
|
-
KEYWORD_TYPES = %i[key keyreq].freeze
|
|
28
|
+
KEYWORD_TYPES = %i[key keyreq].freeze #: Array[Symbol]
|
|
26
29
|
|
|
27
30
|
# Distinguishes "no layout was configured" from "a layout of `nil` was configured", which is
|
|
28
31
|
# how a view opts out.
|
|
29
32
|
#
|
|
30
33
|
# @api private
|
|
31
34
|
# @since 0.2.0
|
|
32
|
-
UNSET = ::Object.new.freeze
|
|
35
|
+
UNSET = ::Object.new.freeze #: Object
|
|
33
36
|
|
|
34
37
|
# @api private
|
|
35
38
|
# @since 0.2.0
|
|
39
|
+
#: (singleton(::Phlex::SGML)) -> void
|
|
36
40
|
def self.included(view_class)
|
|
37
|
-
view_class.
|
|
41
|
+
view_class.include(Contextual)
|
|
38
42
|
view_class.extend(ClassMethods)
|
|
39
|
-
return unless defined?(::Hanami::Helpers::I18nHelper)
|
|
40
|
-
|
|
41
|
-
# Order matters: a module included later sits higher in the lookup chain, so the overrides
|
|
42
|
-
# have to go in after Hanami's helper, not alongside it in this module.
|
|
43
|
-
view_class.include(::Hanami::Helpers::I18nHelper)
|
|
44
|
-
view_class.include(I18nOverrides)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Relative-key resolution for Hanami's i18n helper.
|
|
48
|
-
#
|
|
49
|
-
# A Phlex view has no template, so Hanami's implementation — which reads
|
|
50
|
-
# `_context.current_template_name` — cannot work. Included after `Hanami::Helpers::I18nHelper`
|
|
51
|
-
# so that it wins: a module included later sits higher in the lookup chain.
|
|
52
|
-
#
|
|
53
|
-
# @api private
|
|
54
|
-
# @since 0.2.0
|
|
55
|
-
module I18nOverrides
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# Resolves a relative translation key (`t(".title")`) against the view's own container key, so
|
|
59
|
-
# `MyApp::Views::Posts::Index` looks up `posts.index.title`.
|
|
60
|
-
#
|
|
61
|
-
# Hanami's own implementation uses `_context.current_template_name`, which is hanami-view's
|
|
62
|
-
# template stack and has no meaning for a Phlex view.
|
|
63
|
-
def _resolve_i18n_key(key)
|
|
64
|
-
return key unless key.to_s.start_with?(".")
|
|
65
|
-
|
|
66
|
-
base = i18n_key_base
|
|
67
|
-
unless base
|
|
68
|
-
raise ::I18n::ArgumentError,
|
|
69
|
-
"Cannot resolve the relative translation key #{key.inspect} for #{self.class}. " \
|
|
70
|
-
"Give the view a name inside a slice, or use an absolute key (without a leading dot)."
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
"#{base}#{key}"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# The view's container key, dotted: `MyApp::Views::Posts::Index` in the app slice becomes
|
|
77
|
-
# `posts.index`. Nil for an anonymous view, or one outside any slice.
|
|
78
|
-
def i18n_key_base
|
|
79
|
-
view_slice = self.class.slice
|
|
80
|
-
name = self.class.name
|
|
81
|
-
return nil unless view_slice && name
|
|
82
|
-
|
|
83
|
-
view_slice.inflector
|
|
84
|
-
.underscore(name)
|
|
85
|
-
.sub(%r{^#{view_slice.slice_name.path}/}, "")
|
|
86
|
-
.sub(%r{^views/}, "")
|
|
87
|
-
.tr("/", ".")
|
|
88
|
-
end
|
|
89
43
|
end
|
|
90
44
|
|
|
91
45
|
# @api public
|
|
@@ -111,6 +65,7 @@ module Phlex
|
|
|
111
65
|
#
|
|
112
66
|
# @api public
|
|
113
67
|
# @since 0.2.0
|
|
68
|
+
#: (?context: (::Hanami::View::Context | Context)?, **untyped) -> String
|
|
114
69
|
def call(context: nil, **input)
|
|
115
70
|
phlex_context = { CONTEXT_KEY => context }
|
|
116
71
|
body = new(**accepted_input(input)).call(context: phlex_context)
|
|
@@ -121,17 +76,12 @@ module Phlex
|
|
|
121
76
|
layout_class.new.call(context: phlex_context) { |layout| layout.raw(layout.safe(body)) }
|
|
122
77
|
end
|
|
123
78
|
|
|
124
|
-
# @api private
|
|
125
|
-
# @since 0.2.0
|
|
126
|
-
def configure_for_slice(slice)
|
|
127
|
-
extend SliceConfigured.new(slice)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
79
|
# The layout configured on this class, or inherited from a superclass. {UNSET} when none has
|
|
131
80
|
# been configured anywhere in the chain.
|
|
132
81
|
#
|
|
133
82
|
# @api private
|
|
134
83
|
# @since 0.2.0
|
|
84
|
+
#: () -> (singleton(::Phlex::SGML) | Object | nil)
|
|
135
85
|
def configured_layout
|
|
136
86
|
return @layout if defined?(@layout)
|
|
137
87
|
return superclass.configured_layout if superclass.respond_to?(:configured_layout)
|
|
@@ -165,6 +115,7 @@ module Phlex
|
|
|
165
115
|
#
|
|
166
116
|
# @api public
|
|
167
117
|
# @since 0.2.0
|
|
118
|
+
#: (?singleton(::Phlex::SGML)?) -> singleton(::Phlex::SGML)?
|
|
168
119
|
def layout(layout_class = UNSET)
|
|
169
120
|
unless UNSET.equal?(layout_class)
|
|
170
121
|
@layout = layout_class
|
|
@@ -175,22 +126,13 @@ module Phlex
|
|
|
175
126
|
UNSET.equal?(configured) ? default_layout : configured
|
|
176
127
|
end
|
|
177
128
|
|
|
178
|
-
# The slice this view belongs to, or nil when it is defined outside a slice namespace.
|
|
179
|
-
#
|
|
180
|
-
# @return [Hanami::Slice, nil]
|
|
181
|
-
#
|
|
182
|
-
# @api public
|
|
183
|
-
# @since 0.2.0
|
|
184
|
-
def slice
|
|
185
|
-
nil
|
|
186
|
-
end
|
|
187
|
-
|
|
188
129
|
private
|
|
189
130
|
|
|
190
131
|
# Keeps only the keywords `initialize` declares.
|
|
191
132
|
#
|
|
192
133
|
# A view whose initializer takes `**` opts out and receives everything, request params
|
|
193
134
|
# included.
|
|
135
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
194
136
|
def accepted_input(input)
|
|
195
137
|
parameters = instance_method(:initialize).parameters
|
|
196
138
|
return input if parameters.any? { |type, _| type == :keyrest }
|
|
@@ -200,6 +142,7 @@ module Phlex
|
|
|
200
142
|
|
|
201
143
|
# The slice's conventional layout: `Views::Layout` in the slice's namespace, if it defines
|
|
202
144
|
# one. Resolved on each call rather than memoized, so code reloading is not defeated.
|
|
145
|
+
#: () -> singleton(::Phlex::SGML)?
|
|
203
146
|
def default_layout
|
|
204
147
|
return nil unless slice
|
|
205
148
|
return nil unless slice.namespace.const_defined?(:Views, false)
|
|
@@ -208,172 +151,6 @@ module Phlex
|
|
|
208
151
|
views.const_get(:Layout, false) if views.const_defined?(:Layout, false)
|
|
209
152
|
end
|
|
210
153
|
end
|
|
211
|
-
|
|
212
|
-
# The URL for an asset.
|
|
213
|
-
#
|
|
214
|
-
# @example
|
|
215
|
-
# script(src: asset_url("app.js"))
|
|
216
|
-
#
|
|
217
|
-
# @param source [String] the asset's source path
|
|
218
|
-
#
|
|
219
|
-
# @return [String] the URL
|
|
220
|
-
#
|
|
221
|
-
# @api public
|
|
222
|
-
# @since 0.2.0
|
|
223
|
-
def asset_url(source)
|
|
224
|
-
assets[source].url
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# The slice's assets.
|
|
228
|
-
#
|
|
229
|
-
# @api public
|
|
230
|
-
# @since 0.2.0
|
|
231
|
-
def assets
|
|
232
|
-
hanami_context.assets
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Stores a string of markup for later use, or reads back what was stored.
|
|
236
|
-
#
|
|
237
|
-
# @api public
|
|
238
|
-
# @since 0.2.0
|
|
239
|
-
def content_for(...)
|
|
240
|
-
hanami_context.content_for(...)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# The current request's CSRF token.
|
|
244
|
-
#
|
|
245
|
-
# @return [String]
|
|
246
|
-
#
|
|
247
|
-
# @api public
|
|
248
|
-
# @since 0.2.0
|
|
249
|
-
def csrf_token
|
|
250
|
-
hanami_context.csrf_token
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# The flash hash for the current request.
|
|
254
|
-
#
|
|
255
|
-
# @api public
|
|
256
|
-
# @since 0.2.0
|
|
257
|
-
def flash
|
|
258
|
-
hanami_context.flash
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# The Hanami view context for this render.
|
|
262
|
-
#
|
|
263
|
-
# Read from Phlex's user context, which is shared with every component in the render tree, so
|
|
264
|
-
# a component nested any number of levels deep sees the same context.
|
|
265
|
-
#
|
|
266
|
-
# @return [Object] a `Hanami::View::Context` when hanami-view is bundled, otherwise a {Context}
|
|
267
|
-
#
|
|
268
|
-
# @raise [MissingContextError] when the view was rendered without a context
|
|
269
|
-
#
|
|
270
|
-
# @api public
|
|
271
|
-
# @since 0.2.0
|
|
272
|
-
def hanami_context
|
|
273
|
-
context[CONTEXT_KEY] || raise(MissingContextError, self.class)
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Whether a Hanami view context is available.
|
|
277
|
-
#
|
|
278
|
-
# @return [Boolean]
|
|
279
|
-
#
|
|
280
|
-
# @api public
|
|
281
|
-
# @since 0.2.0
|
|
282
|
-
def hanami_context?
|
|
283
|
-
!context[CONTEXT_KEY].nil?
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# The slice's i18n backend.
|
|
287
|
-
#
|
|
288
|
-
# @api public
|
|
289
|
-
# @since 0.2.0
|
|
290
|
-
def i18n
|
|
291
|
-
hanami_context.i18n
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# The path for a named route.
|
|
295
|
-
#
|
|
296
|
-
# @example
|
|
297
|
-
# a(href: path(:posts)) { "Posts" }
|
|
298
|
-
#
|
|
299
|
-
# @return [String]
|
|
300
|
-
#
|
|
301
|
-
# @api public
|
|
302
|
-
# @since 0.2.0
|
|
303
|
-
def path(...)
|
|
304
|
-
routes.path(...)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# The current request.
|
|
308
|
-
#
|
|
309
|
-
# @return [Hanami::Action::Request]
|
|
310
|
-
#
|
|
311
|
-
# @api public
|
|
312
|
-
# @since 0.2.0
|
|
313
|
-
def request
|
|
314
|
-
hanami_context.request
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Whether the view is being rendered from within a request.
|
|
318
|
-
#
|
|
319
|
-
# @return [Boolean]
|
|
320
|
-
#
|
|
321
|
-
# @api public
|
|
322
|
-
# @since 0.2.0
|
|
323
|
-
def request?
|
|
324
|
-
hanami_context? && hanami_context.request?
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
# The app's routes helper.
|
|
328
|
-
#
|
|
329
|
-
# @return [Hanami::Slice::RoutesHelper]
|
|
330
|
-
#
|
|
331
|
-
# @api public
|
|
332
|
-
# @since 0.2.0
|
|
333
|
-
def routes
|
|
334
|
-
hanami_context.routes
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
# The session for the current request.
|
|
338
|
-
#
|
|
339
|
-
# @api public
|
|
340
|
-
# @since 0.2.0
|
|
341
|
-
def session
|
|
342
|
-
hanami_context.session
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# The slice this view belongs to.
|
|
346
|
-
#
|
|
347
|
-
# @return [Hanami::Slice, nil]
|
|
348
|
-
#
|
|
349
|
-
# @api public
|
|
350
|
-
# @since 0.2.0
|
|
351
|
-
def slice
|
|
352
|
-
self.class.slice
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
# The full URL for a named route.
|
|
356
|
-
#
|
|
357
|
-
# Hanami's routes helper returns a `URI`; Phlex writes strings, and rejects anything else as
|
|
358
|
-
# an attribute value, so this hands back the string.
|
|
359
|
-
#
|
|
360
|
-
# @example
|
|
361
|
-
# a(href: url(:posts)) { "Posts" }
|
|
362
|
-
#
|
|
363
|
-
# @return [String]
|
|
364
|
-
#
|
|
365
|
-
# @api public
|
|
366
|
-
# @since 0.2.0
|
|
367
|
-
def url(...)
|
|
368
|
-
routes.url(...).to_s
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
private
|
|
372
|
-
|
|
373
|
-
# Hanami's helper modules reach for the view context under this name.
|
|
374
|
-
def _context
|
|
375
|
-
hanami_context
|
|
376
|
-
end
|
|
377
154
|
end
|
|
378
155
|
end
|
|
379
156
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "testing"
|
|
4
|
+
|
|
5
|
+
# Wires {Phlex::Hanami::Testing::ViewHelpers} into RSpec. Require it from `spec/spec_helper.rb`, or
|
|
6
|
+
# from a file under `spec/support`, which the spec helper hanami-rspec generates already loads:
|
|
7
|
+
#
|
|
8
|
+
# require "phlex/hanami/rspec"
|
|
9
|
+
#
|
|
10
|
+
# The helpers go into every example group tagged `type: :view`. Tag component specs the same way; a
|
|
11
|
+
# component is a view as far as this is concerned.
|
|
12
|
+
RSpec.configure do |config|
|
|
13
|
+
config.include Phlex::Hanami::Testing::ViewHelpers, type: :view
|
|
14
|
+
end
|
|
@@ -17,10 +17,11 @@ module Phlex
|
|
|
17
17
|
#
|
|
18
18
|
# @api private
|
|
19
19
|
# @since 0.2.0
|
|
20
|
-
attr_reader :slice
|
|
20
|
+
attr_reader :slice #: singleton(::Hanami::Slice)
|
|
21
21
|
|
|
22
22
|
# @api private
|
|
23
23
|
# @since 0.2.0
|
|
24
|
+
#: (singleton(::Hanami::Slice)) -> void
|
|
24
25
|
def initialize(slice)
|
|
25
26
|
super()
|
|
26
27
|
@slice = slice
|
|
@@ -28,18 +29,21 @@ module Phlex
|
|
|
28
29
|
|
|
29
30
|
# @api private
|
|
30
31
|
# @since 0.2.0
|
|
32
|
+
#: (singleton(::Phlex::SGML) | singleton(::Hanami::Mailer)) -> void
|
|
31
33
|
def extended(_klass)
|
|
32
34
|
define_slice
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
# @api private
|
|
36
38
|
# @since 0.2.0
|
|
39
|
+
#: () -> String
|
|
37
40
|
def inspect
|
|
38
41
|
"#<#{self.class.name}[#{slice.name}]>"
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
private
|
|
42
45
|
|
|
46
|
+
#: () -> void
|
|
43
47
|
def define_slice
|
|
44
48
|
slice = @slice
|
|
45
49
|
|
|
@@ -16,10 +16,11 @@ module Phlex
|
|
|
16
16
|
#
|
|
17
17
|
# @api private
|
|
18
18
|
# @since 0.2.0
|
|
19
|
-
attr_reader :slice
|
|
19
|
+
attr_reader :slice #: singleton(::Hanami::Slice)
|
|
20
20
|
|
|
21
21
|
# @api private
|
|
22
22
|
# @since 0.2.0
|
|
23
|
+
#: (singleton(::Hanami::Slice)) -> void
|
|
23
24
|
def initialize(slice)
|
|
24
25
|
super()
|
|
25
26
|
@slice = slice
|
|
@@ -27,12 +28,14 @@ module Phlex
|
|
|
27
28
|
|
|
28
29
|
# @api private
|
|
29
30
|
# @since 0.2.0
|
|
31
|
+
#: (singleton(Context)) -> void
|
|
30
32
|
def extended(_context_class)
|
|
31
33
|
define_new
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# @api private
|
|
35
37
|
# @since 0.2.0
|
|
38
|
+
#: () -> String
|
|
36
39
|
def inspect
|
|
37
40
|
"#<#{self.class.name}[#{slice.name}]>"
|
|
38
41
|
end
|
|
@@ -40,6 +43,7 @@ module Phlex
|
|
|
40
43
|
private
|
|
41
44
|
|
|
42
45
|
# Defines a `.new` that resolves the slice's components and passes them to `#initialize`.
|
|
46
|
+
#: () -> void
|
|
43
47
|
def define_new
|
|
44
48
|
dependencies = method(:dependencies)
|
|
45
49
|
|
|
@@ -50,18 +54,22 @@ module Phlex
|
|
|
50
54
|
|
|
51
55
|
# The slice components a context is injected with, resolved fresh for each context so that
|
|
52
56
|
# a container which does not memoize still hands out working objects.
|
|
57
|
+
#: () -> Hash[Symbol, untyped]
|
|
53
58
|
def dependencies
|
|
54
59
|
{ assets: resolve_assets, i18n: resolve_i18n, inflector: slice.inflector, routes: resolve_routes }
|
|
55
60
|
end
|
|
56
61
|
|
|
62
|
+
#: () -> ::Hanami::Assets?
|
|
57
63
|
def resolve_assets
|
|
58
64
|
slice["assets"] if slice.key?("assets")
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
#: () -> ::Hanami::Providers::I18n::Backend?
|
|
61
68
|
def resolve_i18n
|
|
62
69
|
slice["i18n"] if slice.key?("i18n")
|
|
63
70
|
end
|
|
64
71
|
|
|
72
|
+
#: () -> ::Hanami::Slice::RoutesHelper?
|
|
65
73
|
def resolve_routes
|
|
66
74
|
slice["routes"] if slice.key?("routes")
|
|
67
75
|
end
|