File Coverage

blib/lib/Email/MIME/Kit/Assembler/Markdown.pm
Criterion Covered Total %
statement 88 93 94.6
branch 21 38 55.2
condition n/a
subroutine 10 10 100.0
pod 0 2 0.0
total 119 143 83.2


line stmt bran cond sub pod time code
1             package Email::MIME::Kit::Assembler::Markdown 0.100009;
2             # ABSTRACT: build multipart/alternative messages from Markdown alone
3              
4 1     1   576099 use Moose;
  1         12  
  1         9  
5             with 'Email::MIME::Kit::Role::Assembler';
6              
7 1     1   5816 use Email::MIME 1.900;
  1         16  
  1         24  
8 1     1   5 use Moose::Util::TypeConstraints qw(maybe_type role_type);
  1         2  
  1         10  
9 1     1   1042 use Text::Markdown;
  1         22166  
  1         48  
10 1     1   495 use HTML::Entities ();
  1         4921  
  1         1001  
11              
12             #pod =for Pod::Coverage assemble BUILD
13             #pod
14             #pod =head1 SYNOPSIS
15             #pod
16             #pod In your mkit's (JSON, here) manifest:
17             #pod
18             #pod {
19             #pod "renderer" : "TT",
20             #pod "assembler": [
21             #pod "Markdown",
22             #pod { "html_wrapper": "wrapper.html" }
23             #pod ],
24             #pod "path" : "body.mkdn",
25             #pod "header": [
26             #pod { "Subject": "DynaWoop is now hiring!" },
27             #pod { "From" : "[% from_addr %]" }
28             #pod { "To" : "[% user.email %]" }
29             #pod ]
30             #pod }
31             #pod
32             #pod This kit will build a multipart/alternative message with a plaintext part
33             #pod (containing the rendered contents of F<body.mkdn> ) and an HTML part
34             #pod (containing F<body.mkdn> rendered into HTML using Markdown).
35             #pod
36             #pod At present, attachments are not supported. Actually, quite a few things found
37             #pod in the standard assembler are not yet supported. The standard assembler
38             #pod desperately needs to be refactored to make its features easier to incorporate
39             #pod into other assemblers.
40             #pod
41             #pod The C<html_wrapper> parameter for the Markdown assembler is the path to a kit
42             #pod entry. If given, that kit entry will be used for the HTML part, and the
43             #pod Markdown-produced HTML will be injected into it, replacing a comment containing
44             #pod the C<marker> given in the Markdown assembler's configuration. The default
45             #pod marker is C<CONTENT>, so the F<wrapper.html> used above might read as follows:
46             #pod
47             #pod <h1>DynaWoop Dynamic Woopages</h1>
48             #pod <!-- CONTENT -->
49             #pod <p>Click to unsubscribe: <a href="[% unsub_url %]">here</a></p>
50             #pod
51             #pod The C<text_wrapper> setting works exactly the same way, down to looking for an
52             #pod HTML-like comment containing the marker. It wraps the Markdown content after
53             #pod it has been rendered by the kit's Renderer, if any.
54             #pod
55             #pod If given (and true), the C<munge_signature> option will perform some basic
56             #pod munging of a sigdash-prefixed signature in the source text, hardening line
57             #pod breaks. The specific munging performed is not guaranteed to remain exactly
58             #pod stable.
59             #pod
60             #pod If given (and true), the C<render_wrapper> option will cause the kit entry to
61             #pod be passed through the renderer named in the kit. That is to say, the kit entry
62             #pod is a template. In this case, the C<marker> comment is ignored. Instead, the
63             #pod wrapped content (Markdown-produced HTML or text) is available in a template
64             #pod parameter called C<wrapped_content>, and should be included that way.
65             #pod
66             #pod If given (and true), the C<encode_entities> option will cause HTML in the
67             #pod source text to be entity encoded in the HTML part (and passed through
68             #pod unmodified in the plain text part)
69             #pod
70             #pod The comment C<< <!-- SKIP-LINE --> >> may be included on any line of the source
71             #pod document. Lines containing that substring will not be included in the final
72             #pod plaintext part, but will be included in the HTML part. (The comment itself
73             #pod will be removed.) This allows adding extra HTML to the Markdown without it
74             #pod remaining, annoyingly, in the text part. To change the exact text looked for
75             #pod from C<SKIP-LINE>, you can set the C<skip_marker> attribute of the assembler.
76             #pod
77             #pod =cut
78              
79             has manifest => (
80             is => 'ro',
81             required => 1,
82             );
83              
84             has html_wrapper => (
85             is => 'ro',
86             isa => 'Str',
87             );
88              
89             has text_wrapper => (
90             is => 'ro',
91             isa => 'Str',
92             );
93              
94             has munge_signature => (
95             is => 'ro',
96             # XXX Removed because JSON booly objects (and YAML?) aren't consistently
97             # compatible with Moose's Bool type. -- rjbs, 2016-08-03
98             # isa => 'Bool',
99             default => 0,
100             );
101              
102             has render_wrapper => (
103             is => 'ro',
104             # XXX Removed because JSON booly objects (and YAML?) aren't consistently
105             # compatible with Moose's Bool type. -- rjbs, 2016-08-03
106             # isa => 'Bool',
107             default => 0,
108             );
109              
110             has encode_entities => (
111             is => 'ro',
112             default => 0,
113             );
114              
115             has renderer => (
116             reader => 'renderer',
117             writer => '_set_renderer',
118             clearer => '_unset_renderer',
119             isa => maybe_type(role_type('Email::MIME::Kit::Role::Renderer')),
120             lazy => 1,
121             default => sub { $_[0]->kit->default_renderer },
122             init_arg => undef,
123             );
124              
125             has marker => (is => 'ro', isa => 'Str', default => 'CONTENT');
126              
127             has skip_marker => (is => 'ro', isa => 'Str', default => 'SKIP-LINE');
128              
129             has path => (
130             is => 'ro',
131             isa => 'Str',
132             lazy => 1,
133             default => sub { $_[0]->manifest->{path} },
134             );
135              
136             sub BUILD {
137 6     6 0 15 my ($self) = @_;
138 6         14 my $class = ref $self;
139              
140             confess "$class does not support alternatives"
141 6 50       10 if @{ $self->manifest->{alternatives} || [] };
  6 50       203  
142              
143             confess "$class does not support attachments"
144 6 50       12 if @{ $self->manifest->{attachments} || [] };
  6 50       169  
145              
146             confess "$class does not support MIME content attributes"
147 6 50       9 if %{ $self->manifest->{attributes} || {} };
  6 50       160  
148             }
149              
150             sub _prep_header {
151 6     6   13 my ($self, $header, $stash) = @_;
152              
153 6         10 my @done_header;
154 6         13 for my $entry (@$header) {
155             confess "no field name candidates"
156 12 50       34 unless my (@hval) = grep { /^[^:]/ } keys %$entry;
  12         60  
157 12 50       30 confess "multiple field name candidates: @hval" if @hval > 1;
158 12         26 my $value = $entry->{ $hval[ 0 ] };
159              
160 12 50       23 if (ref $value) {
161 0         0 my ($v, $p) = @$value;
162 0         0 $value = join q{; }, $v, map { "$_=$p->{$_}" } keys %$p;
  0         0  
163             } else {
164 12         413 my $renderer = $self->renderer;
165 12 50       31 if (exists $entry->{':renderer'}) {
166 0 0       0 undef $renderer if ! defined $entry->{':renderer'};
167 0         0 confess 'alternate renderers not supported';
168             }
169              
170 12 50       25 $value = ${ $renderer->render(\$value, $stash) } if defined $renderer;
  12         32  
171             }
172              
173 12         495 push @done_header, $hval[0] => $value;
174             }
175              
176 6         16 return \@done_header;
177             }
178              
179             sub assemble {
180 6     6 0 901 my ($self, $stash) = @_;
181              
182 6         12 my $markdown = ${ $self->kit->get_decoded_kit_entry( $self->path ) };
  6         147  
183 6         1604 my $plaintext = $markdown;
184              
185              
186 6 50       213 if ($self->renderer) {
187             {
188 6         19 local $stash->{part_type} = 'text';
189 6         167 my $output = $self->renderer->render(\$markdown, $stash);
190 6         557 $plaintext = ${ $self->renderer->render(\$markdown, $stash) };
  6         186  
191             }
192              
193             {
194 6         8 local $stash->{part_type} = 'html';
  6         439  
  6         19  
195 6         9 $markdown = ${ $self->renderer->render(\$markdown, $stash) };
  6         188  
196             }
197             }
198              
199             # We'll remove any line containing <!-- SKIP-LINE --> from the plain text
200             # part. Meanwhile, the comment is removed from the Markdown, but the rest of
201             # the line is left intact. -- rjbs, 2021-11-23
202 6         576 my $skip_marker = $self->skip_marker;
203 6         57 $plaintext =~ s{^.*<!--\s+\Q$skip_marker\E\s+-->.*$}{}mg;
204 6         42 $markdown =~ s{<!--\s+\Q$skip_marker\E\s+-->}{}mg;
205              
206 6 100       189 if ($self->encode_entities) {
207 2         11 $markdown = HTML::Entities::encode_entities($markdown);
208             }
209              
210 6 100       234 if ($self->munge_signature) {
211 1         8 my ($body, $sig) = split /^-- $/m, $markdown, 2;
212              
213 1 50       6 if (defined $sig) {
214 1         6 $sig =~ s{^}{<br />}mg;
215 1         5 $markdown = "$body\n\n$sig";
216             }
217             }
218              
219 6         46 my %content = (
220             html => Text::Markdown->new(tab_width => 2)->markdown($markdown),
221             text => $plaintext,
222             );
223              
224 6         27046 for my $type (keys %content) {
225 12         29 my $type_wrapper = "$type\_wrapper";
226              
227 12 50       424 if (my $wrapper_path = $self->$type_wrapper) {
228 12         20 my $wrapper = ${ $self->kit->get_decoded_kit_entry($wrapper_path) };
  12         283  
229              
230 12 100       3276 if ($self->render_wrapper) {
231 4         24 local $stash->{wrapped_content} = $content{$type};
232 4         9 local $stash->{part_type} = $type;
233 4         117 my $output_ref = $self->renderer->render(\$wrapper, $stash);
234 4         470 $content{$type} = $$output_ref;
235             } else {
236 8         234 my $marker = $self->marker;
237 8         50 my $marker_re = qr{<!--\s+\Q$marker\E\s+-->};
238              
239 8 50       69 confess "$type_wrapper does not contain comment containing marker"
240             unless $wrapper =~ $marker_re;
241              
242 8         69 $wrapper =~ s/$marker_re/$content{$type}/;
243 8         34 $content{$type} = $wrapper;
244             }
245             }
246             }
247              
248             my $header = $self->_prep_header(
249             $self->manifest->{header},
250 6         195 $stash,
251             );
252              
253             my $html_part = Email::MIME->create(
254             body_str => $content{html},
255 6         56 attributes => {
256             content_type => "text/html",
257             charset => 'utf-8',
258             encoding => 'quoted-printable',
259             },
260             );
261              
262             my $text_part = Email::MIME->create(
263             body_str => $content{text},
264 6         12446 attributes => {
265             content_type => "text/plain",
266             charset => 'utf-8',
267             encoding => 'quoted-printable',
268             },
269             );
270              
271 6         7900 my $container = Email::MIME->create(
272             header_str => $header,
273             parts => [ $text_part, $html_part ],
274             attributes => { content_type => 'multipart/alternative' },
275             );
276              
277 6         22458 return $container;
278             }
279              
280 1     1   9 no Moose;
  1         2  
  1         11  
281 1     1   235 no Moose::Util::TypeConstraints;
  1         2  
  1         10  
282             __PACKAGE__->meta->make_immutable;
283             1;
284              
285             __END__
286              
287             =pod
288              
289             =encoding UTF-8
290              
291             =head1 NAME
292              
293             Email::MIME::Kit::Assembler::Markdown - build multipart/alternative messages from Markdown alone
294              
295             =head1 VERSION
296              
297             version 0.100009
298              
299             =head1 SYNOPSIS
300              
301             In your mkit's (JSON, here) manifest:
302              
303             {
304             "renderer" : "TT",
305             "assembler": [
306             "Markdown",
307             { "html_wrapper": "wrapper.html" }
308             ],
309             "path" : "body.mkdn",
310             "header": [
311             { "Subject": "DynaWoop is now hiring!" },
312             { "From" : "[% from_addr %]" }
313             { "To" : "[% user.email %]" }
314             ]
315             }
316              
317             This kit will build a multipart/alternative message with a plaintext part
318             (containing the rendered contents of F<body.mkdn> ) and an HTML part
319             (containing F<body.mkdn> rendered into HTML using Markdown).
320              
321             At present, attachments are not supported. Actually, quite a few things found
322             in the standard assembler are not yet supported. The standard assembler
323             desperately needs to be refactored to make its features easier to incorporate
324             into other assemblers.
325              
326             The C<html_wrapper> parameter for the Markdown assembler is the path to a kit
327             entry. If given, that kit entry will be used for the HTML part, and the
328             Markdown-produced HTML will be injected into it, replacing a comment containing
329             the C<marker> given in the Markdown assembler's configuration. The default
330             marker is C<CONTENT>, so the F<wrapper.html> used above might read as follows:
331              
332             <h1>DynaWoop Dynamic Woopages</h1>
333             <!-- CONTENT -->
334             <p>Click to unsubscribe: <a href="[% unsub_url %]">here</a></p>
335              
336             The C<text_wrapper> setting works exactly the same way, down to looking for an
337             HTML-like comment containing the marker. It wraps the Markdown content after
338             it has been rendered by the kit's Renderer, if any.
339              
340             If given (and true), the C<munge_signature> option will perform some basic
341             munging of a sigdash-prefixed signature in the source text, hardening line
342             breaks. The specific munging performed is not guaranteed to remain exactly
343             stable.
344              
345             If given (and true), the C<render_wrapper> option will cause the kit entry to
346             be passed through the renderer named in the kit. That is to say, the kit entry
347             is a template. In this case, the C<marker> comment is ignored. Instead, the
348             wrapped content (Markdown-produced HTML or text) is available in a template
349             parameter called C<wrapped_content>, and should be included that way.
350              
351             If given (and true), the C<encode_entities> option will cause HTML in the
352             source text to be entity encoded in the HTML part (and passed through
353             unmodified in the plain text part)
354              
355             The comment C<< <!-- SKIP-LINE --> >> may be included on any line of the source
356             document. Lines containing that substring will not be included in the final
357             plaintext part, but will be included in the HTML part. (The comment itself
358             will be removed.) This allows adding extra HTML to the Markdown without it
359             remaining, annoyingly, in the text part. To change the exact text looked for
360             from C<SKIP-LINE>, you can set the C<skip_marker> attribute of the assembler.
361              
362             =head1 PERL VERSION
363              
364             This module should work on any version of perl still receiving updates from
365             the Perl 5 Porters. This means it should work on any version of perl released
366             in the last two to three years. (That is, if the most recently released
367             version is v5.40, then this module should work on both v5.40 and v5.38.)
368              
369             Although it may work on older versions of perl, no guarantee is made that the
370             minimum required version will not be increased. The version may be increased
371             for any reason, and there is no promise that patches will be accepted to lower
372             the minimum required perl.
373              
374             =for Pod::Coverage assemble BUILD
375              
376             =head1 AUTHOR
377              
378             Ricardo Signes <cpan@semiotic.systems>
379              
380             =head1 CONTRIBUTORS
381              
382             =for stopwords Chris Nehren Michael McClimon Ricardo Signes Robert Norris
383              
384             =over 4
385              
386             =item *
387              
388             Chris Nehren <cnehren@gmail.com>
389              
390             =item *
391              
392             Michael McClimon <michael@mcclimon.org>
393              
394             =item *
395              
396             Ricardo Signes <rjbs@semiotic.systems>
397              
398             =item *
399              
400             Robert Norris <rob@eatenbyagrue.org>
401              
402             =back
403              
404             =head1 COPYRIGHT AND LICENSE
405              
406             This software is copyright (c) 2022 by Ricardo Signes.
407              
408             This is free software; you can redistribute it and/or modify it under
409             the same terms as the Perl 5 programming language system itself.
410              
411             =cut