phlex-hanami 0.2.0.pre.alpha.1 → 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: d51a4710b34ce6bb8d1a140f7df3fdae00006df8fd23b39c84506297e12f64df
4
- data.tar.gz: 18954a7148a4d46c0b3053dadc7ccd534a25c9276eabe317f01274d9a4ce377a
3
+ metadata.gz: eb2ded5cb201e8636daeba76f21fc25b297899eba517ce428d6fcc41450460f9
4
+ data.tar.gz: f8935646735d5d5e52faf6c48b5713dae2f4169c02e8162fc4356c32e5cf09bd
5
5
  SHA512:
6
- metadata.gz: c3a536625ff7017b56129fd4f151d194240e9dfa4427d3f4d774cb1bb68ef3fd61dd069f7f7b7eef347e90f2339b9596da0ce737df90f1451c6ae81096186b3a
7
- data.tar.gz: 178cedf68bfaed0844171fbfd6a935b9c931901f296c59ba922e957ba7caabf825bbd052bf3ce384255df4df7f90a4aa7108ed5d60e08d3102c8d646e4d64b87
6
+ metadata.gz: 396360b1808f053b39c29047103c6f860c46ac6bf436890a483848c63639c1e382fc0ad34eefb61f58335a4d158783349670c2b0ee30a421473737c5111e2abf
7
+ data.tar.gz: 84fbc91b5c45c6ece40da01070cd64bd9b2702cbc19c10c1ba8eeb3d27cc7aab57e3e60434305b18021dc980cc0a1fff983a051d5cce33b09b200fd87667db52
data/CHANGELOG.md CHANGED
@@ -7,7 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [v0.2.0-alpha.1] - 2024-10-05
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
11
39
 
12
40
  Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aaronmallen).
13
41
 
@@ -15,5 +43,7 @@ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aa
15
43
 
16
44
  Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
17
45
 
18
- [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.1.0...HEAD
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
19
49
  [v0.1.0]: https://github.com/stephannv/phlex-hanami/releases/tag/v0.1.0
@@ -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,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
@@ -53,7 +53,9 @@ module Phlex
53
53
  # Defines `Views::Context` for the slice when nothing else has.
54
54
  #
55
55
  # Does nothing when hanami-view is bundled, because Hanami defines its own richer context
56
- # there, and nothing when the user has defined one themselves.
56
+ # there, and returns the existing one when the user has defined it themselves.
57
+ #
58
+ # @return [Class, nil] the slice's context class
57
59
  #
58
60
  # @api private
59
61
  # @since 0.2.0
@@ -61,7 +63,7 @@ module Phlex
61
63
  return if ::Hanami.bundled?("hanami-view")
62
64
 
63
65
  namespace = views_namespace(slice)
64
- return if namespace.const_defined?(:Context, false)
66
+ return namespace.const_get(:Context, false) if namespace.const_defined?(:Context, false)
65
67
 
66
68
  # The class is anonymous at this point, so the slice cannot be inferred from its name
67
69
  # the way `Hanami::SliceConfigurable` would; configure it explicitly instead.
@@ -83,6 +85,22 @@ module Phlex
83
85
  component_dirs.instance = COMPONENT_INSTANCE
84
86
  end
85
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
+
86
104
  private
87
105
 
88
106
  def views_namespace(slice)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Mailer
6
+ # The base class for mail layouts.
7
+ #
8
+ # Name one `Views::Mailers::Layout` in a slice's namespace and every mail view in that slice
9
+ # renders inside it. The web layout is never used for mail; set a different one with `layout`,
10
+ # or opt out with `layout nil`.
11
+ #
12
+ # The layout is part of both message parts: the text part is converted from the HTML the
13
+ # layout produced, so a footer written once appears in both.
14
+ #
15
+ # @example
16
+ # # app/views/mailers/layout.rb
17
+ # module MyApp
18
+ # module Views
19
+ # module Mailers
20
+ # class Layout < Phlex::Hanami::Mailer::Layout
21
+ # def view_template
22
+ # html do
23
+ # body do
24
+ # yield
25
+ # p { "Sent by MyApp" }
26
+ # end
27
+ # end
28
+ # end
29
+ # end
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ # @api public
35
+ # @since 0.2.0
36
+ class Layout < Phlex::HTML
37
+ include Renderable
38
+
39
+ # A layout is never itself wrapped in a layout.
40
+ layout nil
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Mailer
6
+ # Makes a Phlex class renderable by a Hanami mailer.
7
+ #
8
+ # Include this into an existing `Phlex::SGML` subclass, or subclass {View}, which includes it
9
+ # already. It builds on {Phlex::Hanami::Renderable} and adds the three things mail needs: a
10
+ # context with no request behind it, a second format for the plain text part, and a layout
11
+ # convention of its own.
12
+ #
13
+ # @example
14
+ # module MyApp
15
+ # class MailerView < Phlex::HTML
16
+ # include Phlex::Hanami::Mailer::Renderable
17
+ # end
18
+ # end
19
+ #
20
+ # @api public
21
+ # @since 0.2.0
22
+ module Renderable
23
+ # @api private
24
+ # @since 0.2.0
25
+ def self.included(view_class)
26
+ view_class.include(::Phlex::Hanami::Renderable)
27
+
28
+ # Order matters: a module included later sits higher in the lookup chain, so the mail
29
+ # methods have to go in after `Renderable`, not alongside them in this module.
30
+ view_class.include(MailMethods)
31
+ view_class.extend(ClassMethods)
32
+ end
33
+
34
+ # @api public
35
+ # @since 0.2.0
36
+ module ClassMethods
37
+ # Renders one part of a mail message.
38
+ #
39
+ # This is what `Hanami::Mailer#render_view` calls, once per part, with the format naming
40
+ # the part it wants. Hanami's mailer has no view context to hand over — there is no
41
+ # request to build one from — so a mail view builds the slice's own context itself.
42
+ #
43
+ # @param context [Object, nil] the Hanami view context, built from the slice when omitted
44
+ # @param format [Symbol] `:html` or `:text`
45
+ # @param input [Hash] the mailer's exposures
46
+ #
47
+ # @return [String] the rendered part
48
+ #
49
+ # @api public
50
+ # @since 0.2.0
51
+ def call(context: nil, format: :html, **input)
52
+ html = super(context: context || mail_context, **input)
53
+ return html unless format.to_s == "text"
54
+
55
+ new(**accepted_input(input)).text_body(html)
56
+ end
57
+
58
+ private
59
+
60
+ # A slice's conventional mail layout: `Views::Mailers::Layout`, if it defines one.
61
+ #
62
+ # The web layout is the wrong one for an email — it carries the stylesheets, scripts and
63
+ # page chrome no mail client wants — so mail looks for its own, and renders without a
64
+ # layout when the slice has none.
65
+ def default_layout
66
+ return nil unless slice
67
+ return nil unless slice.namespace.const_defined?(:Views, false)
68
+
69
+ views = slice.namespace.const_get(:Views, false)
70
+ return nil unless views.const_defined?(:Mailers, false)
71
+
72
+ mailers = views.const_get(:Mailers, false)
73
+ mailers.const_get(:Layout, false) if mailers.const_defined?(:Layout, false)
74
+ end
75
+
76
+ # The slice's view context, built without a request.
77
+ #
78
+ # The same class an action would be given, so a view reads the same in both places. What
79
+ # a request would have filled in — `request`, `session`, `flash`, `csrf_token` — is
80
+ # missing, and the context says so when a view reaches for it.
81
+ def mail_context
82
+ return nil unless slice
83
+
84
+ Extensions::Slice.view_context_class(slice).new
85
+ end
86
+ end
87
+
88
+ # What a mail view does differently.
89
+ #
90
+ # Included after `Phlex::Hanami::Renderable` so that it wins: a module included later sits
91
+ # higher in the lookup chain.
92
+ #
93
+ # @api private
94
+ # @since 0.2.0
95
+ module MailMethods
96
+ # Raises. An email is read outside the app, so a link in one needs a full URL.
97
+ #
98
+ # @raise [RelativePathError]
99
+ #
100
+ # @api public
101
+ # @since 0.2.0
102
+ def path(*, **)
103
+ raise RelativePathError, self.class
104
+ end
105
+
106
+ # The plain text part of the message.
107
+ #
108
+ # Called with the rendered HTML, layout and all, and converts it by default. Override it
109
+ # to write the text part by hand; the view's own state is there to read.
110
+ #
111
+ # @example
112
+ # def text_body(_html)
113
+ # "Welcome, #{@user.name}. Get started: #{url(:root)}"
114
+ # end
115
+ #
116
+ # @param html [String] the rendered HTML part
117
+ #
118
+ # @return [String] the text part
119
+ #
120
+ # @api public
121
+ # @since 0.2.0
122
+ def text_body(html)
123
+ Text.call(html)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Mailer
6
+ # Turns the HTML part of a message into its plain text alternative.
7
+ #
8
+ # This is regex over markup Phlex has just produced, not a general HTML parser. It knows the
9
+ # handful of things that matter in an email — anchors, lists, line breaks, block elements and
10
+ # the entities Phlex escapes — and leaves everything else on the floor. Markup from somewhere
11
+ # else, pasted into a view with `raw`, may not come out well.
12
+ #
13
+ # {Renderable#text_body} calls this. Override that method to write the text part by hand.
14
+ #
15
+ # @api public
16
+ # @since 0.2.0
17
+ class Text
18
+ # Elements that end a line of text, and so become a blank line.
19
+ #
20
+ # @api private
21
+ # @since 0.2.0
22
+ BLOCK_ELEMENTS = %w[
23
+ article blockquote div footer h1 h2 h3 h4 h5 h6 header hr li ol p section table td tr ul
24
+ ].freeze
25
+
26
+ # @api private
27
+ # @since 0.2.0
28
+ BLOCK_PATTERN = %r{</?(?:#{BLOCK_ELEMENTS.join('|')})\b[^>]*>}i
29
+
30
+ # The entities Phlex escapes, and the characters they stand for.
31
+ #
32
+ # @api private
33
+ # @since 0.2.0
34
+ ENTITIES = {
35
+ "&#39;" => "'",
36
+ "&amp;" => "&",
37
+ "&apos;" => "'",
38
+ "&gt;" => ">",
39
+ "&lt;" => "<",
40
+ "&nbsp;" => " ",
41
+ "&quot;" => '"',
42
+ }.freeze
43
+
44
+ # @api private
45
+ # @since 0.2.0
46
+ ENTITY_PATTERN = /&(?:amp|apos|gt|lt|nbsp|quot|#39);/
47
+
48
+ # Stand in for the angle brackets around a URL until the tags are gone, so that stripping
49
+ # tags does not eat the link the converter has just written.
50
+ #
51
+ # @api private
52
+ # @since 0.2.0
53
+ LINK_CLOSE = "\u0011"
54
+
55
+ # @api private
56
+ # @since 0.2.0
57
+ LINK_OPEN = "\u0010"
58
+
59
+ class << self
60
+ # Converts rendered HTML to plain text.
61
+ #
62
+ # @param html [String] the rendered HTML part
63
+ #
64
+ # @return [String] the text part
65
+ #
66
+ # @api public
67
+ # @since 0.2.0
68
+ def call(html)
69
+ text = body_of(html.to_s)
70
+ text = strip_hidden_elements(text)
71
+ text = expand_anchors(text)
72
+ text = expand_breaks(text)
73
+ text = expand_list_items(text)
74
+ text = expand_blocks(text)
75
+ text = strip_tags(text)
76
+ text = unescape(text)
77
+ restore_links(tidy(text))
78
+ end
79
+
80
+ private
81
+
82
+ # `Label <https://example.com/path>`, or one of the two when the other adds nothing.
83
+ def anchor_text(attributes, inner)
84
+ url = href_in(attributes)
85
+ label = strip_tags(inner).gsub(/\s+/, " ").strip
86
+ return label if url.empty?
87
+ return url if label.empty? || label == url
88
+
89
+ "#{label} #{LINK_OPEN}#{url}#{LINK_CLOSE}"
90
+ end
91
+
92
+ def body_of(html) = html[%r{<body\b[^>]*>(.*?)</body>}mi, 1] || html
93
+
94
+ def expand_anchors(html)
95
+ html.gsub(%r{<a\b([^>]*)>(.*?)</a>}mi) { anchor_text(Regexp.last_match(1), Regexp.last_match(2)) }
96
+ end
97
+
98
+ def expand_blocks(html) = html.gsub(BLOCK_PATTERN, "\n\n")
99
+
100
+ def expand_breaks(html) = html.gsub(%r{<br\b[^>]*/?>}i, "\n")
101
+
102
+ def expand_list_items(html) = html.gsub(%r{</li\s*>}i, "").gsub(/<li\b[^>]*>/i, "\n- ")
103
+
104
+ def href_in(attributes)
105
+ match = attributes.match(/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i)
106
+ return "" unless match
107
+
108
+ (match[1] || match[2] || match[3]).to_s.strip
109
+ end
110
+
111
+ def restore_links(text) = text.gsub(LINK_OPEN, "<").gsub(LINK_CLOSE, ">")
112
+
113
+ def strip_hidden_elements(html) = html.gsub(%r{<(head|style|script)\b[^>]*>.*?</\1\s*>}mi, "")
114
+
115
+ def strip_tags(html) = html.gsub(/<!--.*?-->/m, "").gsub(/<[^>]*>/, "")
116
+
117
+ def tidy(text) = text.gsub(/[ \t]+/, " ").gsub(/[ \t]+$/, "").gsub(/\n{3,}/, "\n\n").strip
118
+
119
+ def unescape(text) = text.gsub(ENTITY_PATTERN) { ENTITIES.fetch(Regexp.last_match(0)) }
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ module Mailer
6
+ # The base class for Phlex views rendered by a Hanami mailer.
7
+ #
8
+ # Subclass this for a mail view, or for an app-level base mail view. If you already have your
9
+ # own `Phlex::HTML` base class, include {Renderable} into it instead — the two are equivalent.
10
+ #
11
+ # A mailer finds its view by name, the way an action does: `MyApp::Mailers::Welcome` renders
12
+ # `MyApp::Views::Mailers::Welcome`.
13
+ #
14
+ # @example
15
+ # # app/views/mailers/welcome.rb
16
+ # module MyApp
17
+ # module Views
18
+ # module Mailers
19
+ # class Welcome < Phlex::Hanami::Mailer::View
20
+ # def initialize(user:) = @user = user
21
+ #
22
+ # def view_template
23
+ # h1 { "Welcome, #{@user.name}" }
24
+ # p { a(href: url(:root)) { "Get started" } }
25
+ # end
26
+ # end
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # @api public
32
+ # @since 0.2.0
33
+ class View < Phlex::HTML
34
+ include Renderable
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Phlex views for hanami-mailer.
6
+ #
7
+ # A mailer renders the same view twice, once for each part of the message, so a mail view
8
+ # answers to a format: {Mailer::View} renders markup for the HTML part and converts that markup
9
+ # for the text part. Everything else a view can do it can do too, except the parts of the
10
+ # context that only exist during a request.
11
+ #
12
+ # @example
13
+ # # app/mailers/welcome.rb
14
+ # module MyApp
15
+ # module Mailers
16
+ # class Welcome < Hanami::Mailer
17
+ # from "hello@example.com"
18
+ # to { |user| user.email }
19
+ # subject "Welcome"
20
+ #
21
+ # expose :user
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # # app/views/mailers/welcome.rb
27
+ # module MyApp
28
+ # module Views
29
+ # module Mailers
30
+ # class Welcome < Phlex::Hanami::Mailer::View
31
+ # def initialize(user:) = @user = user
32
+ #
33
+ # def view_template
34
+ # h1 { "Welcome, #{@user.name}" }
35
+ # p { a(href: url(:root)) { "Get started" } }
36
+ # end
37
+ # end
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # @api public
43
+ # @since 0.2.0
44
+ module Mailer
45
+ end
46
+ end
47
+ end
@@ -124,7 +124,7 @@ module Phlex
124
124
  # @api private
125
125
  # @since 0.2.0
126
126
  def configure_for_slice(slice)
127
- extend SliceConfiguredView.new(slice)
127
+ extend SliceConfigured.new(slice)
128
128
  end
129
129
 
130
130
  # The layout configured on this class, or inherited from a superclass. {UNSET} when none has
@@ -354,12 +354,18 @@ module Phlex
354
354
 
355
355
  # The full URL for a named route.
356
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
+ #
357
363
  # @return [String]
358
364
  #
359
365
  # @api public
360
366
  # @since 0.2.0
361
367
  def url(...)
362
- routes.url(...)
368
+ routes.url(...).to_s
363
369
  end
364
370
 
365
371
  private
@@ -2,15 +2,15 @@
2
2
 
3
3
  module Phlex
4
4
  module Hanami
5
- # Provides slice-specific configuration to a {Renderable} view defined in a slice's namespace.
5
+ # Tells a class which slice it belongs to.
6
6
  #
7
- # `Hanami::SliceConfigurable` calls `configure_for_slice` once per slice as views are defined,
8
- # so a view in the app namespace is configured for the app and one in a slice namespace is
9
- # configured for that slice.
7
+ # `Hanami::SliceConfigurable` calls `configure_for_slice` once per slice as classes are defined,
8
+ # so a class in the app namespace is configured for the app and one in a slice namespace is
9
+ # configured for that slice. {Renderable} views and Hanami mailers both extend one of these.
10
10
  #
11
11
  # @api private
12
12
  # @since 0.2.0
13
- class SliceConfiguredView < Module
13
+ class SliceConfigured < Module
14
14
  # The slice this module configures for.
15
15
  #
16
16
  # @return [Hanami::Slice]
@@ -28,7 +28,7 @@ module Phlex
28
28
 
29
29
  # @api private
30
30
  # @since 0.2.0
31
- def extended(_view_class)
31
+ def extended(_klass)
32
32
  define_slice
33
33
  end
34
34
 
data/lib/phlex/hanami.rb CHANGED
@@ -42,3 +42,7 @@ end.setup
42
42
  # Installs the integration. Referencing the constant is what autoloads it, so this cannot move into
43
43
  # the file itself — nothing else would ever refer to it.
44
44
  Hanami::Slice::ClassMethods.prepend(Phlex::Hanami::Extensions::Slice)
45
+
46
+ # Mail is a second entry point, and an optional one. Zeitwerk keeps the mail classes unloaded until
47
+ # something names one, but prepending onto `Hanami::Mailer` needs the constant to exist.
48
+ Phlex::Hanami::Extensions::Mailer.install if Hanami.bundled?("hanami-mailer")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-hanami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.pre.alpha.1
4
+ version: 0.2.0.pre.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
@@ -66,13 +66,21 @@ files:
66
66
  - lib/phlex/hanami.rb
67
67
  - lib/phlex/hanami/context.rb
68
68
  - lib/phlex/hanami/errors/error.rb
69
+ - lib/phlex/hanami/errors/mailer_view_error.rb
69
70
  - lib/phlex/hanami/errors/missing_context_error.rb
71
+ - lib/phlex/hanami/errors/relative_path_error.rb
72
+ - lib/phlex/hanami/extensions/mailer.rb
70
73
  - lib/phlex/hanami/extensions/slice.rb
71
74
  - lib/phlex/hanami/helpers.rb
72
75
  - lib/phlex/hanami/layout.rb
76
+ - lib/phlex/hanami/mailer.rb
77
+ - lib/phlex/hanami/mailer/layout.rb
78
+ - lib/phlex/hanami/mailer/renderable.rb
79
+ - lib/phlex/hanami/mailer/text.rb
80
+ - lib/phlex/hanami/mailer/view.rb
73
81
  - lib/phlex/hanami/renderable.rb
82
+ - lib/phlex/hanami/slice_configured.rb
74
83
  - lib/phlex/hanami/slice_configured_context.rb
75
- - lib/phlex/hanami/slice_configured_view.rb
76
84
  - lib/phlex/hanami/view.rb
77
85
  - sig/phlex-hanami.rbs
78
86
  homepage: https://github.com/aaronmallen/phlex-hanami