phlex-hanami 0.1.0 → 0.2.0.pre.alpha.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 066d1770e9e59fc2e7a2a1e5a333be685bdee0c94180efd155a578af2f47d224
4
- data.tar.gz: dab7797c0500df8bdaeaf8c856628e9b6a876e88e95511fc5bdbe18f4a09c1a7
3
+ metadata.gz: eb2ded5cb201e8636daeba76f21fc25b297899eba517ce428d6fcc41450460f9
4
+ data.tar.gz: f8935646735d5d5e52faf6c48b5713dae2f4169c02e8162fc4356c32e5cf09bd
5
5
  SHA512:
6
- metadata.gz: 3acf4d4ec8b5ada0374b05f249cfe838351b93b5ac06a41f5876543b9b8b0725b442e89f7fe7414a827bcc7290fa7a4849dab25ed75b0802e2c762d7913788c9
7
- data.tar.gz: 72f5b9ba2a7bc3aa46c16d4d548eebacd5825efe6d7476cc29bb1167cb0b5547a5ffd18fcf5edb1eca732dff1ce777478696597ae86529d5190b7da64fb1dfa6
6
+ metadata.gz: 396360b1808f053b39c29047103c6f860c46ac6bf436890a483848c63639c1e382fc0ad34eefb61f58335a4d158783349670c2b0ee30a421473737c5111e2abf
7
+ data.tar.gz: 84fbc91b5c45c6ece40da01070cd64bd9b2702cbc19c10c1ba8eeb3d27cc7aab57e3e60434305b18021dc980cc0a1fff983a051d5cce33b09b200fd87667db52
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
1
8
  ## [Unreleased]
2
9
 
3
- ## [0.1.0] - 2024-12-07
10
+ ## [v0.2.0-alpha.2] - 2026-09-05
11
+
12
+ ### Added
13
+
14
+ - Phlex views for [hanami-mailer]. A mailer renders the Phlex class whose container key matches its own, so
15
+ `MyApp::Mailers::Welcome` renders `MyApp::Views::Mailers::Welcome` with no configuration.
16
+ - `Phlex::Hanami::Mailer::View` and `Phlex::Hanami::Mailer::Layout`, and `Phlex::Hanami::Mailer::Renderable` for an
17
+ existing `Phlex::HTML` base class.
18
+ - A plain text alternative part, converted from the rendered HTML by `Phlex::Hanami::Mailer::Text`. Override
19
+ `text_body` on the view to write it by hand.
20
+ - A mail layout convention: `Views::Mailers::Layout` in a slice wraps that slice's mail views.
21
+ - Usage documentation under [docs/usage](docs/usage/README.md), covering views, layouts, the view context,
22
+ helpers, mailers and how the gem hooks into Hanami.
23
+ - `Phlex::Hanami::MailerViewError`, raised when a mailer is paired with a Phlex view that is not a mail view,
24
+ rather than sending a message whose plain text part is markup.
25
+
26
+ ### Changed
27
+
28
+ - `url` in a view returns a String. Hanami's routes helper returns a `URI`, which Phlex rejects as an attribute
29
+ value, so `a(href: url(:posts))` raised.
30
+
31
+ ### Fixed
32
+
33
+ - `path` in a mail view raises `Phlex::Hanami::RelativePathError` instead of writing a relative link an email client
34
+ cannot follow.
35
+
36
+ [hanami-mailer]: https://github.com/hanami/mailer
37
+
38
+ ## [v0.2.0-alpha.1] - 2026-09-04
39
+
40
+ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aaronmallen).
41
+
42
+ ## [v0.1.0] - 2024-12-07
43
+
44
+ Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
4
45
 
5
- - Initial release
46
+ [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...HEAD
47
+ [v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
48
+ [v0.2.0-alpha.1]: https://github.com/aaronmallen/phlex-hanami/releases/tag/0.2.0-alpha.1
49
+ [v0.1.0]: https://github.com/stephannv/phlex-hanami/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
- The MIT License (MIT)
1
+ MIT License
2
2
 
3
- Copyright (c) 2024 stephann
3
+ Copyright (c) 2026 Aaron Allen
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
9
  copies of the Software, and to permit persons to whom the Software is
10
10
  furnished to do so, subject to the following conditions:
11
11
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
14
 
15
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # The view context used when the hanami-view gem is not bundled.
6
+ #
7
+ # Hanami builds a context once per request and hands it to the view as `context:`. When
8
+ # hanami-view is bundled that context is a `Hanami::View::Context` and this class is unused;
9
+ # otherwise phlex-hanami defines a `Views::Context` subclass of this in every slice.
10
+ #
11
+ # The public surface mirrors `Hanami::View::Context` so that a view reads the same either way.
12
+ #
13
+ # @api public
14
+ # @since 0.2.0
15
+ class Context
16
+ extend ::Hanami::SliceConfigurable
17
+
18
+ # @api private
19
+ # @since 0.2.0
20
+ def self.configure_for_slice(slice)
21
+ extend SliceConfiguredContext.new(slice)
22
+ end
23
+
24
+ # The app's inflector.
25
+ #
26
+ # @return [Dry::Inflector]
27
+ #
28
+ # @api public
29
+ # @since 0.2.0
30
+ attr_reader :inflector
31
+
32
+ # @see SliceConfiguredContext#define_new
33
+ #
34
+ # @api private
35
+ # @since 0.2.0
36
+ def initialize(inflector: nil, routes: nil, assets: nil, request: nil, i18n: nil, **)
37
+ @inflector = inflector
38
+ @routes = routes
39
+ @assets = assets
40
+ @request = request
41
+ @i18n = i18n
42
+ @content_for = {}
43
+ end
44
+
45
+ # The slice's assets.
46
+ #
47
+ # @return [Hanami::Assets]
48
+ #
49
+ # @raise [Hanami::ComponentLoadError] when hanami-assets is not bundled, or no assets exist
50
+ #
51
+ # @api public
52
+ # @since 0.2.0
53
+ def assets
54
+ raise ::Hanami::ComponentLoadError, "Assets not available. #{assets_hint}" unless @assets
55
+
56
+ @assets
57
+ end
58
+
59
+ # Store a string of markup for later use, or read back what was stored.
60
+ #
61
+ # @param key [Symbol] the content key
62
+ # @param value [String, nil] the content, when no block is given
63
+ #
64
+ # @return [String, nil]
65
+ #
66
+ # @api public
67
+ # @since 0.2.0
68
+ def content_for(key, value = nil)
69
+ if block_given?
70
+ @content_for[key] = yield
71
+ nil
72
+ elsif value
73
+ @content_for[key] = value
74
+ nil
75
+ else
76
+ @content_for[key]
77
+ end
78
+ end
79
+
80
+ # The current request's CSRF token.
81
+ #
82
+ # @return [String]
83
+ #
84
+ # @raise [Hanami::ComponentLoadError] when there is no request
85
+ #
86
+ # @api public
87
+ # @since 0.2.0
88
+ def csrf_token
89
+ request.session[::Hanami::Action::CSRFProtection::CSRF_TOKEN]
90
+ end
91
+
92
+ # The flash hash for the current request.
93
+ #
94
+ # @raise [Hanami::ComponentLoadError] when there is no request
95
+ #
96
+ # @api public
97
+ # @since 0.2.0
98
+ def flash
99
+ request.flash
100
+ end
101
+
102
+ # The slice's i18n backend.
103
+ #
104
+ # @raise [Hanami::ComponentLoadError] when the i18n gem is not bundled
105
+ #
106
+ # @api public
107
+ # @since 0.2.0
108
+ def i18n
109
+ raise ::Hanami::ComponentLoadError, "the i18n gem is required to access translations" unless @i18n
110
+
111
+ @i18n
112
+ end
113
+
114
+ # @api private
115
+ # @since 0.2.0
116
+ def initialize_copy(source)
117
+ super
118
+ @content_for = source.instance_variable_get(:@content_for).dup
119
+ end
120
+
121
+ # The current request, when the view is rendered from an action.
122
+ #
123
+ # @return [Hanami::Action::Request]
124
+ #
125
+ # @raise [Hanami::ComponentLoadError] when the view is not rendered from a request
126
+ #
127
+ # @api public
128
+ # @since 0.2.0
129
+ def request
130
+ unless @request
131
+ raise ::Hanami::ComponentLoadError, <<~MESSAGE
132
+ Request not available. Only views rendered from Hanami::Action instances have a request.
133
+ MESSAGE
134
+ end
135
+
136
+ @request
137
+ end
138
+
139
+ # Whether a request is available.
140
+ #
141
+ # @return [Boolean]
142
+ #
143
+ # @api public
144
+ # @since 0.2.0
145
+ def request?
146
+ !!@request
147
+ end
148
+
149
+ # The app's routes helper.
150
+ #
151
+ # @return [Hanami::Slice::RoutesHelper]
152
+ #
153
+ # @raise [Hanami::ComponentLoadError] when hanami-router is not bundled or routes are undefined
154
+ #
155
+ # @api public
156
+ # @since 0.2.0
157
+ def routes
158
+ raise ::Hanami::ComponentLoadError, "the hanami-router gem is required to access routes" unless @routes
159
+
160
+ @routes
161
+ end
162
+
163
+ # The session for the current request.
164
+ #
165
+ # @raise [Hanami::ComponentLoadError] when there is no request
166
+ #
167
+ # @api public
168
+ # @since 0.2.0
169
+ def session
170
+ request.session
171
+ end
172
+
173
+ private
174
+
175
+ def assets_hint
176
+ if ::Hanami.bundled?("hanami-assets")
177
+ "Have you put files into your assets directory?"
178
+ else
179
+ "The hanami-assets gem is required to access assets."
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Base class for every error raised by phlex-hanami.
6
+ #
7
+ # @api public
8
+ # @since 0.2.0
9
+ class Error < StandardError
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Raised when a mailer's paired Phlex view cannot render a mail message.
6
+ #
7
+ # A mailer asks its view for two parts, and an ordinary view answers both with markup — which
8
+ # would ship an email whose plain text part is HTML. Better to say so than to send it.
9
+ #
10
+ # @api public
11
+ # @since 0.2.0
12
+ class MailerViewError < Error
13
+ # @api private
14
+ # @since 0.2.0
15
+ def initialize(mailer_class, view_class)
16
+ super(<<~MESSAGE)
17
+ #{mailer_class} is paired with #{view_class}, which is not a mail view.
18
+
19
+ A mail view renders both parts of the message. Subclass the mail base class:
20
+
21
+ class #{view_class} < Phlex::Hanami::Mailer::View
22
+
23
+ or include the module into the base class you already have:
24
+
25
+ include Phlex::Hanami::Mailer::Renderable
26
+ MESSAGE
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Raised when a view reaches for the Hanami view context but was rendered without one.
6
+ #
7
+ # @api public
8
+ # @since 0.2.0
9
+ class MissingContextError < Error
10
+ # @api private
11
+ # @since 0.2.0
12
+ def initialize(view_class)
13
+ super(<<~MESSAGE)
14
+ #{view_class} was rendered without a Hanami view context.
15
+
16
+ Views rendered from an action are given one automatically. When rendering a view yourself,
17
+ pass one:
18
+
19
+ #{view_class}.call(context: MyApp::Views::Context.new)
20
+ MESSAGE
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Raised when a mail view asks for a relative path.
6
+ #
7
+ # An email is read outside the app, so a relative path in one is a dead link. Mail views raise
8
+ # rather than render one.
9
+ #
10
+ # @api public
11
+ # @since 0.2.0
12
+ class RelativePathError < Error
13
+ # @api private
14
+ # @since 0.2.0
15
+ def initialize(view_class)
16
+ super(<<~MESSAGE)
17
+ #{view_class} asked for a relative path, and an email needs a full URL.
18
+
19
+ Use `url` instead:
20
+
21
+ a(href: url(:post, id: @post.id)) { @post.title }
22
+ MESSAGE
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Extensions
6
+ # Pairs a Hanami mailer with a Phlex view.
7
+ #
8
+ # Prepended onto `Hanami::Mailer`, so it sits in front of hanami-mailer's own view handling
9
+ # and answers first. A mailer's view is any object taking `call(format:, **input)`, which a
10
+ # {Phlex::Hanami::Mailer::View} already is, so pairing is the whole of the integration:
11
+ # nothing here teaches hanami-mailer what Phlex is.
12
+ #
13
+ # @api private
14
+ # @since 0.2.0
15
+ module Mailer
16
+ # Prepends this onto `Hanami::Mailer`.
17
+ #
18
+ # Called from `phlex/hanami.rb` when hanami-mailer is bundled. Zeitwerk keeps the mail
19
+ # classes unloaded until something names one, but a prepend needs the constant, so the
20
+ # check belongs at the call site.
21
+ #
22
+ # @api private
23
+ # @since 0.2.0
24
+ def self.install
25
+ ::Hanami::Mailer.prepend(self)
26
+ ::Hanami::Mailer.singleton_class.prepend(ClassMethods)
27
+ end
28
+
29
+ # @api private
30
+ # @since 0.2.0
31
+ module ClassMethods
32
+ # @api private
33
+ # @since 0.2.0
34
+ def configure_for_slice(slice)
35
+ super
36
+ extend SliceConfigured.new(slice)
37
+ end
38
+
39
+ # The mailer's paired Phlex view, or nil when it has none.
40
+ #
41
+ # The key is derived the way hanami-mailer derives a template name: underscore the class
42
+ # name and drop the slice's own segment, so `MyApp::Mailers::Welcome` looks up
43
+ # `views.mailers.welcome`. Resolved through the container on every call rather than
44
+ # memoized, so code reloading is not defeated.
45
+ #
46
+ # @return [Class, nil]
47
+ #
48
+ # @raise [MailerViewError] when the paired view is a Phlex class that cannot render mail
49
+ #
50
+ # @api private
51
+ # @since 0.2.0
52
+ def phlex_view
53
+ mailer_slice = slice
54
+ return nil unless mailer_slice && name
55
+
56
+ key = "views.#{view_name(mailer_slice)}"
57
+ return nil unless mailer_slice.key?(key)
58
+
59
+ view = mailer_slice[key]
60
+ return nil unless view.is_a?(::Class) && view < ::Phlex::SGML
61
+ raise MailerViewError.new(self, view) unless view < ::Phlex::Hanami::Mailer::Renderable
62
+
63
+ view
64
+ end
65
+
66
+ # The slice this mailer belongs to, or nil when it is defined outside a slice namespace.
67
+ #
68
+ # @return [Hanami::Slice, nil]
69
+ #
70
+ # @api private
71
+ # @since 0.2.0
72
+ def slice
73
+ nil
74
+ end
75
+
76
+ private
77
+
78
+ def view_name(mailer_slice)
79
+ mailer_slice.inflector
80
+ .underscore(name)
81
+ .sub(%r{^#{mailer_slice.slice_name.path}/}, "")
82
+ .tr("/", ".")
83
+ end
84
+ end
85
+
86
+ # The view this mailer renders.
87
+ #
88
+ # A view passed to the constructor wins, then the paired Phlex view, then whatever
89
+ # hanami-mailer would have used on its own — so an app can render some of its mail with
90
+ # Phlex and the rest with templates.
91
+ #
92
+ # @return [Object, nil]
93
+ #
94
+ # @api private
95
+ # @since 0.2.0
96
+ def view
97
+ @view || self.class.phlex_view || super
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Extensions
6
+ # Installs phlex-hanami into every slice, including the app.
7
+ #
8
+ # Prepended onto `Hanami::Slice::ClassMethods` so that it runs for the app and each of its
9
+ # slices as they are prepared. Two things happen:
10
+ #
11
+ # 1. Phlex classes are registered in the slice container as classes rather than instances.
12
+ # `Dry::System` would otherwise call `new` on them while resolving, which raises for any
13
+ # view with a real initializer, and a memoized Phlex instance can only render once.
14
+ # 2. A `Views::Context` is defined for the slice when hanami-view is not bundled, filling the
15
+ # third-party slot `Hanami::Extensions::Action::SliceConfiguredAction#view_context_class`
16
+ # looks in.
17
+ #
18
+ # @api private
19
+ # @since 0.2.0
20
+ module Slice
21
+ # Returns Phlex classes as classes, and everything else as the loader would.
22
+ #
23
+ # `component.loader.constant` is what the default autoloading loader already calls, so no
24
+ # extra loading happens here and a constant that fails to load still raises
25
+ # `Dry::System::ComponentNotLoadableError`.
26
+ #
27
+ # @api private
28
+ # @since 0.2.0
29
+ COMPONENT_INSTANCE = proc { |component, *args, **kwargs|
30
+ constant = component.loader.constant(component)
31
+
32
+ if constant.is_a?(Class) && constant < Phlex::SGML
33
+ constant
34
+ else
35
+ component.loader.call(component, *args, **kwargs)
36
+ end
37
+ }
38
+
39
+ # @api private
40
+ # @since 0.2.0
41
+ def prepare(provider_name = nil)
42
+ result = super
43
+
44
+ unless provider_name
45
+ Slice.register_phlex_components(self)
46
+ Slice.define_view_context(self)
47
+ end
48
+
49
+ result
50
+ end
51
+
52
+ class << self
53
+ # Defines `Views::Context` for the slice when nothing else has.
54
+ #
55
+ # Does nothing when hanami-view is bundled, because Hanami defines its own richer context
56
+ # there, and returns the existing one when the user has defined it themselves.
57
+ #
58
+ # @return [Class, nil] the slice's context class
59
+ #
60
+ # @api private
61
+ # @since 0.2.0
62
+ def define_view_context(slice)
63
+ return if ::Hanami.bundled?("hanami-view")
64
+
65
+ namespace = views_namespace(slice)
66
+ return namespace.const_get(:Context, false) if namespace.const_defined?(:Context, false)
67
+
68
+ # The class is anonymous at this point, so the slice cannot be inferred from its name
69
+ # the way `Hanami::SliceConfigurable` would; configure it explicitly instead.
70
+ namespace.const_set(:Context, ::Class.new(Context).tap { |klass| klass.configure_for_slice(slice) })
71
+ end
72
+
73
+ # Installs {COMPONENT_INSTANCE} as the default for the slice container's component dirs.
74
+ #
75
+ # `Dry::System::Config::ComponentDirs` re-applies its defaults every time the dirs are
76
+ # read, and only where a dir has not configured the setting itself, so setting this after
77
+ # the dirs have been added covers all of them and clobbers nothing.
78
+ #
79
+ # @api private
80
+ # @since 0.2.0
81
+ def register_phlex_components(slice)
82
+ component_dirs = slice.container.config.component_dirs
83
+ return if component_dirs.configured?(:instance)
84
+
85
+ component_dirs.instance = COMPONENT_INSTANCE
86
+ end
87
+
88
+ # The class an action's view context is built from, for anything rendering outside a
89
+ # request that needs the same one.
90
+ #
91
+ # Reads the third-party slot `Hanami::Extensions::Action::SliceConfiguredAction` reads,
92
+ # so a mail view and a request view are given the same class.
93
+ #
94
+ # @return [Class]
95
+ #
96
+ # @api private
97
+ # @since 0.2.0
98
+ def view_context_class(slice)
99
+ return ::Hanami::Extensions::View::Context.context_class(slice) if ::Hanami.bundled?("hanami-view")
100
+
101
+ define_view_context(slice)
102
+ end
103
+
104
+ private
105
+
106
+ def views_namespace(slice)
107
+ if slice.namespace.const_defined?(:Views, false)
108
+ slice.namespace.const_get(:Views, false)
109
+ else
110
+ slice.namespace.const_set(:Views, ::Module.new)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,9 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+ require "hanami/extensions/view/standard_helpers"
5
+
6
+ # Hanami's helpers return `SafeString`s meant to be interpolated into a template. Marking the class
7
+ # as a Phlex safe object lets Phlex emit them without escaping — both through `raw` and, more often,
8
+ # as the return value of a block.
9
+ Hanami::View::HTML::SafeString.include(Phlex::SGML::SafeObject)
10
+
1
11
  module Phlex
2
12
  module Hanami
13
+ # Hanami's standard helper library, for Phlex views that want it.
14
+ #
15
+ # Opt in per base class; it is not included in {View}, because it adds around twenty methods and
16
+ # needs the hanami-view gem, and neither should depend on what happens to be in your Gemfile.
17
+ #
18
+ # @example
19
+ # module MyApp
20
+ # class View < Phlex::Hanami::View
21
+ # include Phlex::Hanami::Helpers
22
+ # end
23
+ # end
24
+ #
25
+ # class Edit < MyApp::View
26
+ # def view_template
27
+ # raw form_for("post", path(:posts)) { |f| f.text_field(:title) + f.submit("Save") }
28
+ # end
29
+ # end
30
+ #
31
+ # Most of Hanami's helpers duplicate something Phlex already does better — Phlex escapes by
32
+ # default and is itself a tag builder — so reach for `form_for`, `format_number` and the asset
33
+ # helpers, and write everything else as ordinary Phlex.
34
+ #
35
+ # @api public
36
+ # @since 0.2.0
3
37
  module Helpers
4
- def self.included(base)
5
- base.include AssetsHelper
6
- base.include ContextHelper
38
+ # Hanami's helper library defines two methods Phlex already owns.
39
+ #
40
+ # Both of Hanami's return strings to interpolate; Phlex's write into the output buffer.
41
+ # Letting Hanami's win breaks `raw` silently — the helper's output simply never appears — and
42
+ # `raw` is the method you need in order to emit a helper's result at all.
43
+ #
44
+ # @api private
45
+ # @since 0.2.0
46
+ OVERRIDDEN_METHODS = {
47
+ raw: Phlex::SGML.instance_method(:raw),
48
+ tag: Phlex::HTML.instance_method(:tag),
49
+ }.freeze
50
+
51
+ # Phlex's implementations, mixed back in over Hanami's.
52
+ #
53
+ # @api private
54
+ # @since 0.2.0
55
+ PhlexMethods = ::Module.new do
56
+ OVERRIDDEN_METHODS.each do |name, method|
57
+ define_method(name) { |*args, **options, &block| method.bind_call(self, *args, **options, &block) }
58
+ end
59
+ end
60
+
61
+ # @api private
62
+ # @since 0.2.0
63
+ def self.included(view_class)
64
+ view_class.include(::Hanami::Extensions::View::StandardHelpers)
65
+ view_class.include(PhlexMethods)
7
66
  end
8
67
  end
9
68
  end