File Coverage

blib/lib/Email/MIME/Kit/Assembler/Markdown.pm
Criterion Covered Total %
statement 72 77 93.5
branch 19 36 52.7
condition n/a
subroutine 9 9 100.0
pod 0 2 0.0
total 100 124 80.6


line stmt bran cond sub pod time code
1             package Email::MIME::Kit::Assembler::Markdown;
2             # ABSTRACT: build multipart/alternative messages from Markdown alone
3             $Email::MIME::Kit::Assembler::Markdown::VERSION = '0.100005';
4 1     1   395936 use Moose;
  1         2  
  1         6  
5             with 'Email::MIME::Kit::Role::Assembler';
6              
7 1     1   4524 use Email::MIME 1.900;
  1         28  
  1         22  
8 1     1   4 use Moose::Util::TypeConstraints qw(maybe_type role_type);
  1         1  
  1         7  
9 1     1   871 use Text::Markdown;
  1         18299  
  1         713  
10              
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 =cut
66              
67             has manifest => (
68             is => 'ro',
69             required => 1,
70             );
71              
72             has html_wrapper => (
73             is => 'ro',
74             isa => 'Str',
75             );
76              
77             has text_wrapper => (
78             is => 'ro',
79             isa => 'Str',
80             );
81              
82             has munge_signature => (
83             is => 'ro',
84             # XXX Removed because JSON booly objects (and YAML?) aren't consistently
85             # compatible with Moose's Bool type. -- rjbs, 2016-08-03
86             # isa => 'Bool',
87             default => 0,
88             );
89              
90             has render_wrapper => (
91             is => 'ro',
92             # XXX Removed because JSON booly objects (and YAML?) aren't consistently
93             # compatible with Moose's Bool type. -- rjbs, 2016-08-03
94             # isa => 'Bool',
95             default => 0,
96             );
97              
98             has renderer => (
99             reader => 'renderer',
100             writer => '_set_renderer',
101             clearer => '_unset_renderer',
102             isa => maybe_type(role_type('Email::MIME::Kit::Role::Renderer')),
103             lazy => 1,
104             default => sub { $_[0]->kit->default_renderer },
105             init_arg => undef,
106             );
107              
108             has marker => (is => 'ro', isa => 'Str', default => 'CONTENT');
109              
110             has path => (
111             is => 'ro',
112             isa => 'Str',
113             lazy => 1,
114             default => sub { $_[0]->manifest->{path} },
115             );
116              
117             sub BUILD {
118 3     3 0 4 my ($self) = @_;
119 3         4 my $class = ref $self;
120              
121             confess "$class does not support alternatives"
122 3 50       2 if @{ $self->manifest->{alternatives} || [] };
  3 50       102  
123              
124             confess "$class does not support attachments"
125 3 50       4 if @{ $self->manifest->{attachments} || [] };
  3 50       89  
126              
127             confess "$class does not support MIME content attributes"
128 3 50       5 if %{ $self->manifest->{attributes} || {} };
  3 50       83  
129             }
130              
131             sub _prep_header {
132 3     3   4 my ($self, $header, $stash) = @_;
133              
134 3         3 my @done_header;
135 3         6 for my $entry (@$header) {
136             confess "no field name candidates"
137 6 50       16 unless my (@hval) = grep { /^[^:]/ } keys %$entry;
  6         29  
138 6 50       10 confess "multiple field name candidates: @hval" if @hval > 1;
139 6         10 my $value = $entry->{ $hval[ 0 ] };
140              
141 6 50       8 if (ref $value) {
142 0         0 my ($v, $p) = @$value;
143 0         0 $value = join q{; }, $v, map { "$_=$p->{$_}" } keys %$p;
  0         0  
144             } else {
145 6         182 my $renderer = $self->renderer;
146 6 50       12 if (exists $entry->{':renderer'}) {
147 0 0       0 undef $renderer if ! defined $entry->{':renderer'};
148 0         0 confess 'alternate renderers not supported';
149             }
150              
151 6 50       10 $value = ${ $renderer->render(\$value, $stash) } if defined $renderer;
  6         13  
152             }
153              
154 6         334 push @done_header, $hval[0] => $value;
155             }
156              
157 3         6 return \@done_header;
158             }
159              
160             sub assemble {
161 3     3 0 456 my ($self, $stash) = @_;
162              
163 3         4 my $markdown = ${ $self->kit->get_decoded_kit_entry( $self->path ) };
  3         72  
164 3 50       786 if ($self->renderer) {
165 3         84 my $output_ref = $self->renderer->render(\$markdown, $stash);
166 3         212 $markdown = $$output_ref;
167             }
168              
169 3         4 my $plaintext = $markdown;
170              
171 3 100       99 if ($self->munge_signature) {
172 1         7 my ($body, $sig) = split /^-- $/m, $markdown, 2;
173              
174 1 50       4 if (defined $sig) {
175 1         6 $sig =~ s{^}{<br />}mg;
176 1         24 $markdown = "$body\n\n$sig";
177             }
178             }
179              
180 3         22 my %content = (
181             html => Text::Markdown->new(tab_width => 2)->markdown($markdown),
182             text => $plaintext,
183             );
184              
185 3         12780 for my $type (keys %content) {
186 6         12 my $type_wrapper = "$type\_wrapper";
187              
188 6 50       212 if (my $wrapper_path = $self->$type_wrapper) {
189 6         5 my $wrapper = ${ $self->kit->get_decoded_kit_entry($wrapper_path) };
  6         139  
190              
191 6 100       1254 if ($self->render_wrapper) {
192 2         12 $stash->{wrapped_content} = $content{$type};
193 2         59 my $output_ref = $self->renderer->render(\$wrapper, $stash);
194 2         212 $content{$type} = $$output_ref;
195             } else {
196 4         116 my $marker = $self->marker;
197 4         25 my $marker_re = qr{<!--\s+\Q$marker\E\s+-->};
198              
199 4 50       33 confess "$type_wrapper does not contain comment containing marker"
200             unless $wrapper =~ $marker_re;
201              
202 4         30 $wrapper =~ s/$marker_re/$content{$type}/;
203 4         12 $content{$type} = $wrapper;
204             }
205             }
206             }
207              
208             my $header = $self->_prep_header(
209             $self->manifest->{header},
210 3         92 $stash,
211             );
212              
213             my $html_part = Email::MIME->create(
214             body_str => $content{html},
215 3         28 attributes => {
216             content_type => "text/html",
217             charset => 'utf-8',
218             encoding => 'quoted-printable',
219             },
220             );
221              
222             my $text_part = Email::MIME->create(
223             body_str => $content{text},
224 3         5319 attributes => {
225             content_type => "text/plain",
226             charset => 'utf-8',
227             encoding => 'quoted-printable',
228             },
229             );
230              
231 3         2049 my $container = Email::MIME->create(
232             header_str => $header,
233             parts => [ $text_part, $html_part ],
234             attributes => { content_type => 'multipart/alternative' },
235             );
236              
237 3         5579 return $container;
238             }
239              
240 1     1   7 no Moose;
  1         1  
  1         8  
241 1     1   155 no Moose::Util::TypeConstraints;
  1         1  
  1         6  
242             __PACKAGE__->meta->make_immutable;
243             1;
244              
245             __END__
246              
247             =pod
248              
249             =encoding UTF-8
250              
251             =head1 NAME
252              
253             Email::MIME::Kit::Assembler::Markdown - build multipart/alternative messages from Markdown alone
254              
255             =head1 VERSION
256              
257             version 0.100005
258              
259             =head1 SYNOPSIS
260              
261             In your mkit's (JSON, here) manifest:
262              
263             {
264             "renderer" : "TT",
265             "assembler": [
266             "Markdown",
267             { "html_wrapper": "wrapper.html" }
268             ],
269             "path" : "body.mkdn",
270             "header": [
271             { "Subject": "DynaWoop is now hiring!" },
272             { "From" : "[% from_addr %]" }
273             { "To" : "[% user.email %]" }
274             ]
275             }
276              
277             This kit will build a multipart/alternative message with a plaintext part
278             (containing the rendered contents of F<body.mkdn> ) and an HTML part
279             (containing F<body.mkdn> rendered into HTML using Markdown).
280              
281             At present, attachments are not supported. Actually, quite a few things found
282             in the standard assembler are not yet supported. The standard assembler
283             desperately needs to be refactored to make its features easier to incorporate
284             into other assemblers.
285              
286             The C<html_wrapper> parameter for the Markdown assembler is the path to a kit
287             entry. If given, that kit entry will be used for the HTML part, and the
288             Markdown-produced HTML will be injected into it, replacing a comment containing
289             the C<marker> given in the Markdown assembler's configuration. The default
290             marker is C<CONTENT>, so the F<wrapper.html> used above might read as follows:
291              
292             <h1>DynaWoop Dynamic Woopages</h1>
293             <!-- CONTENT -->
294             <p>Click to unsubscribe: <a href="[% unsub_url %]">here</a></p>
295              
296             The C<text_wrapper> setting works exactly the same way, down to looking for an
297             HTML-like comment containing the marker. It wraps the Markdown content after
298             it has been rendered by the kit's Renderer, if any.
299              
300             If given (and true), the C<munge_signature> option will perform some basic
301             munging of a sigdash-prefixed signature in the source text, hardening line
302             breaks. The specific munging performed is not guaranteed to remain exactly
303             stable.
304              
305             If given (and true), the C<render_wrapper> option will cause the kit entry to
306             be passed through the renderer named in the kit. That is to say, the kit entry
307             is a template. In this case, the C<marker> comment is ignored. Instead, the
308             wrapped content (Markdown-produced HTML or text) is available in a template
309             parameter called C<wrapped_content>, and should be included that way.
310              
311             =for Pod::Coverage assemble BUILD
312              
313             =head1 AUTHOR
314              
315             Ricardo Signes <rjbs@cpan.org>
316              
317             =head1 CONTRIBUTORS
318              
319             =for stopwords Chris Nehren Robert Norris
320              
321             =over 4
322              
323             =item *
324              
325             Chris Nehren <cnehren@gmail.com>
326              
327             =item *
328              
329             Robert Norris <rob@eatenbyagrue.org>
330              
331             =back
332              
333             =head1 COPYRIGHT AND LICENSE
334              
335             This software is copyright (c) 2016 by Ricardo Signes.
336              
337             This is free software; you can redistribute it and/or modify it under
338             the same terms as the Perl 5 programming language system itself.
339              
340             =cut