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