File Coverage

blib/lib/Facebook/InstantArticle.pm
Criterion Covered Total %
statement 117 151 77.4
branch 30 74 40.5
condition 1 3 33.3
subroutine 27 31 87.1
pod 16 16 100.0
total 191 275 69.4


line stmt bran cond sub pod time code
1             package Facebook::InstantArticle;
2 2     2   604911 use Moose;
  2         728364  
  2         16  
3 2     2   13402 use namespace::autoclean;
  2         5668  
  2         34  
4              
5 2     2   992 use Mojo::Log;
  2         151946  
  2         34  
6 2     2   966 use XML::Generator;
  2         11845  
  2         13  
7              
8 2     2   700 use Facebook::InstantArticle::Analytics;
  2         6  
  2         87  
9 2     2   799 use Facebook::InstantArticle::Author;
  2         6  
  2         107  
10 2     2   783 use Facebook::InstantArticle::Blockquote;
  2         7  
  2         86  
11 2     2   769 use Facebook::InstantArticle::Copyright;
  2         8  
  2         83  
12 2     2   755 use Facebook::InstantArticle::Embed;
  2         7  
  2         86  
13 2     2   828 use Facebook::InstantArticle::Figure::Image;
  2         8  
  2         93  
14 2     2   780 use Facebook::InstantArticle::Figure::Video;
  2         32  
  2         68  
15 2     2   613 use Facebook::InstantArticle::Heading;
  2         7  
  2         90  
16 2     2   808 use Facebook::InstantArticle::List;
  2         7  
  2         96  
17 2     2   856 use Facebook::InstantArticle::Map;
  2         7  
  2         90  
18 2     2   800 use Facebook::InstantArticle::Paragraph;
  2         9  
  2         2831  
19              
20             our $VERSION = '0.13';
21              
22             =encoding utf-8
23              
24             =head1 NAME
25              
26             Facebook::InstantArticle - Helper class for generating Facebook Instant Articles
27             markup.
28              
29             =head1 DESCRIPTION
30              
31             Facebook::InstantArticle is a simple helper class for generating L<Facebook
32             Instant Articles markup|https://developers.facebook.com/docs/instant-articles/reference>.
33              
34             At the moment it doesn't support all of the features, and both the internal and
35             external API are subject to change in upcoming releases, so use with care.
36              
37             =head1 SYNOPSIS
38              
39             use Facebook::InstantArticle;
40             use DateTime;
41              
42             my $now = DateTime->now,
43              
44             my $ia = Facebook::InstantArticle->new(
45             language => 'en',
46             url => 'http://www.example.com/2016/08/17/some-article',
47             title => 'Some title',
48             subtitle => 'Got one?',
49             kicker => 'Nobody needs a kicker, but...',
50             published => "$now",
51             modified => "$now",
52             auto_ad_placement => 0, # defaults to true
53             style => 'MyStyleName',
54             );
55              
56             $ia->add_author(
57             name => 'Me Myself',
58             description => 'A little bit about myself',
59             );
60              
61             $ia->add_author(
62             name => 'Someone Else',
63             description => 'A little bit about someone else',
64             );
65              
66             $ia->add_lead_asset_image(
67             source => 'http://www.example.com/some_image.png',
68             caption => 'Nice image, eh?',
69             );
70              
71             # or
72              
73             $ia->add_lead_asset_video(
74             source => 'http://www.example.com/some_video.mp4',
75             caption => 'Nice video, eh?',
76             );
77              
78             $ia->add_paragraph(
79             'Will be wrapped in a P element, conversion of inner HTML might be
80             done, explained later in this documentation.'
81             );
82              
83             $ia->add_image(
84             source => 'http://www.example.com/some_image.png',
85             caption => 'Nice picture, eh?',
86             enable_comments => 1, # default false
87             enable_likes => 1, # default false
88             );
89              
90             $ia->add_video(
91             source => 'http://www.example.com/some_video.mp4',
92             caption => 'Nice video, eh?',
93             );
94              
95             say $ia->to_string;
96              
97             =cut
98              
99             has 'language' => ( isa => 'Str', is => 'rw', required => 1 );
100             has 'url' => ( isa => 'Str', is => 'rw', required => 1 );
101             has 'title' => ( isa => 'Str', is => 'rw', required => 1 );
102             has 'subtitle' => ( isa => 'Str', is => 'rw', required => 0 );
103             has 'kicker' => ( isa => 'Str', is => 'rw', required => 0 );
104             has 'published' => ( isa => 'Str', is => 'rw', required => 1 );
105             has 'modified' => ( isa => 'Str', is => 'rw', required => 1 );
106             has 'style' => ( isa => 'Str', is => 'rw', required => 0 );
107             has 'auto_ad_placement' => ( isa => 'Bool', is => 'rw', required => 0, default => 1 );
108              
109             has '_header_elements' => ( isa => 'ArrayRef[Object]', is => 'ro', default => sub { [] } );
110             has '_body_elements' => ( isa => 'ArrayRef[Object]', is => 'ro', default => sub { [] } );
111             has '_footer_elements' => ( isa => 'ArrayRef[Object]', is => 'ro', default => sub { [] } );
112             has '_credit_elements' => ( isa => 'ArrayRef[Object]', is => 'ro', default => sub { [] } );
113              
114             has '_log' => ( isa => 'Mojo::Log', is => 'ro', default => sub { Mojo::Log->new } );
115              
116             =head1 METHODS
117              
118             =head2 add_lead_asset_image
119              
120             Adds a lead asset image to the article.
121              
122             $ia->add_lead_asset_image(
123             source => 'http://www.example.com/lead_image.png',
124             caption => 'Something wicked this way comes...',
125             );
126              
127             =cut
128              
129             sub add_lead_asset_image {
130 1     1 1 1266 my $self = shift;
131              
132 1 50       16 if ( my $e = Facebook::InstantArticle::Figure::Image->new(@_) ) {
133 1 50       2125 if ( $e->is_valid ) {
134 1         2 return push( @{$self->_header_elements}, $e );
  1         28  
135             }
136             }
137              
138 0         0 $self->_log->warn( 'Failed to add lead asset image to article header!' );
139             }
140              
141             =head2 add_lead_asset_video
142              
143             Adds a lead asset video to the article.
144              
145             $ia->add_lead_asset_video(
146             source => 'http://www.example.com/lead_video.mp4',
147             caption => 'Something wicked this way comes...',
148             );
149              
150             =cut
151              
152             sub add_lead_asset_video {
153 0     0 1 0 my $self = shift;
154              
155 0 0       0 if ( my $e = Facebook::InstantArticle::Figure::Video->new(@_) ) {
156 0 0       0 if ( $e->is_valid ) {
157 0         0 return push( @{$self->_header_elements}, $e );
  0         0  
158             }
159             }
160              
161 0         0 $self->_log->warn( 'Failed to add lead asset video to article header!' );
162             }
163              
164             =head2 add_author
165              
166             Adds an author to the article.
167              
168             $ia->add_author(
169             name => 'Oscar Wilde',
170             );
171              
172             =cut
173              
174             sub add_author {
175 1     1 1 7 my $self = shift;
176              
177 1 50       11 if ( my $e = Facebook::InstantArticle::Author->new(@_) ) {
178 1 50       596 if ( $e->is_valid ) {
179 1         2 return push( @{$self->_header_elements}, $e );
  1         26  
180             }
181             }
182              
183 0         0 $self->_log->warn( 'Failed to add author to article header!' );
184             }
185              
186             =head2 add_paragraph
187              
188             Adds a paragraph to the article.
189              
190             $ia->add_paragraph( 'This is a paragraph' );
191              
192             =cut
193              
194             sub add_paragraph {
195 3     3 1 12 my $self = shift;
196              
197 3 50       17 if ( my $e = Facebook::InstantArticle::Paragraph->new(@_) ) {
198 3 100       1736 if ( $e->is_valid ) {
199 2         4 return push( @{$self->_body_elements}, $e );
  2         50  
200             }
201             }
202              
203 1         26 $self->_log->warn( 'Failed to add paragraph to article body!' );
204             }
205              
206             =head2 add_image
207              
208             Adds an image to the article.
209              
210             $ia->add_image(
211             source => 'http://www.example.com/image.png',
212             caption => 'Some caption...',
213             );
214              
215             =cut
216              
217             sub add_image {
218 0     0 1 0 my $self = shift;
219              
220 0 0       0 if ( my $e = Facebook::InstantArticle::Figure::Image->new(@_) ) {
221 0 0       0 if ( $e->is_valid ) {
222 0         0 return push( @{$self->_body_elements}, $e );
  0         0  
223             }
224             }
225              
226 0         0 $self->_log->warn( 'Failed to add image to article body!' );
227             }
228              
229             =head2 add_video
230              
231             Adds a video to the article.
232              
233             $ia->add_video(
234             source => 'http://www.example.com/video.mp4',
235             caption => 'Some caption...',
236             );
237              
238             =cut
239              
240             sub add_video {
241 1     1 1 602 my $self = shift;
242              
243 1 50       11 if ( my $e = Facebook::InstantArticle::Figure::Video->new(@_) ) {
244 1 50       1862 if ( $e->is_valid ) {
245 1         3 return push( @{$self->_body_elements}, $e );
  1         25  
246             }
247             }
248              
249 0         0 $self->_log->warn( 'Failed to add video to article body!' );
250             }
251              
252             =head2 add_slideshow
253              
254             Adds a Facebook::InstantArticle::Slideshow object to the article.
255              
256             my $ss = Facebook::InstantArticle::Slideshow->new;
257              
258             $ss->add_image(
259             source => 'http://www.example.com/image_01.png',
260             caption => 'Image #1',
261             );
262              
263             $ss->add_image(
264             source => 'http://www.example.com/image_02.png',
265             caption => 'Image #2',
266             );
267              
268             $ia->add_slideshow( $ss );
269              
270             =cut
271              
272             sub add_slideshow {
273 1     1 1 7 my $self = shift;
274 1         2 my $slideshow = shift;
275              
276 1 50 33     40 if ( defined $slideshow && $slideshow->is_valid ) {
277 1         2 return push( @{$self->_body_elements}, $slideshow );
  1         28  
278             }
279              
280 0         0 $self->_log->warn( 'Failed to add slideshow to article body!' );
281             }
282              
283             =head2 add_credit
284              
285             Adds a credit to the article.
286              
287             $ia->add_credit( 'Thanks for helping me write this article, someone!' );
288              
289             =cut
290              
291             sub add_credit {
292 1     1 1 6 my $self = shift;
293              
294 1 50       3 if ( my $e = Facebook::InstantArticle::Paragraph->new(@_) ) {
295 1 50       632 if ( $e->is_valid ) {
296 1         3 return push( @{$self->_credit_elements}, $e );
  1         27  
297             }
298             }
299              
300 0         0 $self->_log->warn( 'Failed to add credit to article credits!' );
301             }
302              
303             =head2 add_copyright
304              
305             Adds a copyright to the article.
306              
307             $ia->add_copyright( 'Copyright 2016, Fubar Inc.' );
308              
309             =cut
310              
311             sub add_copyright {
312 1     1 1 6 my $self = shift;
313              
314 1 50       10 if ( my $e = Facebook::InstantArticle::Copyright->new(@_) ) {
315 1 50       650 if ( $e->is_valid ) {
316 1         2 return push( @{$self->_footer_elements}, $e );
  1         29  
317             }
318             }
319              
320 0         0 $self->_log->warn( 'Failed to add copyright to article footer!' );
321             }
322              
323             =head2 add_list
324              
325             Adds a Facebook::InstantArticle::List object to the article.
326              
327             $ia->add_list(
328             ordered => 1, # default 0
329             elements => [ 'Element #1', 'Element #2', 'Element 3' ],
330             );
331              
332             =cut
333              
334             sub add_list {
335 1     1 1 9 my $self = shift;
336              
337 1 50       9 if ( my $e = Facebook::InstantArticle::List->new(@_) ) {
338 1 50       1093 if ( $e->is_valid ) {
339 1         2 return push( @{$self->_body_elements}, $e );
  1         26  
340             }
341             }
342              
343 0         0 $self->_log->warn( 'Failed to add list to article body!' );
344             }
345              
346             =head2 add_blockquote
347              
348             Adds a blockquote to the article.
349              
350             $ia->add_blockquote( 'This is blockquoted.' );
351              
352             =cut
353              
354             sub add_blockquote {
355 1     1 1 6 my $self = shift;
356              
357 1 50       11 if ( my $e = Facebook::InstantArticle::Blockquote->new(@_) ) {
358 1 50       641 if ( $e->is_valid ) {
359 1         3 return push( @{$self->_body_elements}, $e );
  1         32  
360             }
361             }
362              
363 0         0 $self->_log->warn( 'Failed to add blockquote to article body!' );
364             }
365              
366             =head2 add_embed
367              
368             Adds an embed to the article.
369              
370             $ia->add_embed( 'code' );
371              
372             =cut
373              
374             sub add_embed {
375 1     1 1 6 my $self = shift;
376              
377 1 50       11 if ( my $e = Facebook::InstantArticle::Embed->new(@_) ) {
378 1 50       1565 if ( $e->is_valid ) {
379 1         3 return push( @{$self->_body_elements}, $e );
  1         26  
380             }
381             }
382              
383 0         0 $self->_log->warn( 'Failed to add embed to article body!' );
384             }
385              
386             =head2 add_heading
387              
388             Adds a heading to the article BODY.
389              
390             $ia->add_heading(
391             level => 1,
392             text => 'Heading',
393             );
394              
395             =cut
396              
397             sub add_heading {
398 0     0 1 0 my $self = shift;
399              
400 0 0       0 if ( my $e = Facebook::InstantArticle::Heading->new(@_) ) {
401 0 0       0 if ( $e->is_valid ) {
402 0         0 return push( @{$self->_body_elements}, $e );
  0         0  
403             }
404             }
405              
406 0         0 $self->_log->warn( 'Failed to add heading to article body!' );
407             }
408              
409             =head2 add_map
410              
411             Adds a map to the article BODY.
412              
413             $ia->add_map(
414             latitude => 56.1341342,
415             longitude => 23.253474,
416             );
417              
418             =cut
419              
420             sub add_map {
421 1     1 1 6 my $self = shift;
422              
423 1 50       10 if ( my $e = Facebook::InstantArticle::Map->new(@_) ) {
424 1 50       1212 if ( $e->is_valid ) {
425 1         2 return push( @{$self->_body_elements}, $e );
  1         27  
426             }
427             }
428              
429 0         0 $self->_log->warn( 'Failed to add map to article body!' );
430             }
431              
432             =head2 add_analytics
433              
434             Adds an analytics iframe to the article body.
435              
436             =cut
437              
438             sub add_analytics {
439 0     0 1 0 my $self = shift;
440              
441 0 0       0 if ( my $e = Facebook::InstantArticle::Analytics->new(@_) ) {
442 0 0       0 if ( $e->is_valid ) {
443 0         0 return push( @{$self->_body_elements}, $e );
  0         0  
444             }
445             }
446              
447 0         0 $self->_log->warn( 'Failed to add analytics/tracking to article body!' );
448             }
449              
450             =head2 to_string
451              
452             Generates the instant article and returns it as a string.
453              
454             =cut
455              
456             sub to_string {
457 1     1 1 6 my $self = shift;
458              
459             # TODO: Validate
460              
461 1         23 my $gen = XML::Generator->new( ':pretty' );
462              
463             my $xml = $gen->html(
464             { lang => $self->language, prefix => 'op:http://media.facebook.com/op#' },
465              
466             $gen->head(
467             $gen->meta( { charset => 'utf-8' } ),
468             $gen->meta( { property => 'op:markup_version', version => 'v1.0' } ),
469             $gen->meta( { property => 'fb:likes_and_comments', content => 'enable' } ),
470             $gen->meta( { property => 'fb:use_automatic_ad_placement', content => ( $self->auto_ad_placement ? 'enable=true ad_density=default' : 'enable=false' ) } ),
471             ( length $self->style ? $gen->meta( { property => 'fb:article_style', content => $self->style } ) : undef ),
472             $gen->link( { rel => 'canonical', href => $self->url } ),
473             ),
474              
475             $gen->body(
476             $gen->article(
477             $gen->header(
478             $gen->h1( $self->title ),
479             ( length $self->subtitle ? $gen->h2($self->subtitle) : undef ),
480             ( length $self->kicker ? $gen->h3({ class => 'op-kicker' }, $self->kicker) : undef ),
481             $gen->time( { class => 'op-published', datetime => $self->published } ),
482             $gen->time( { class => 'op-modified', datetime => $self->modified } ),
483 1         169 ( @{$self->_header_elements} ? map { $_->as_xml_gen } @{$self->_header_elements} : undef ),
  2         66  
  1         27  
484             ),
485 1         488 ( @{$self->_body_elements} ? map { $_->as_xml_gen } @{$self->_body_elements} : undef ),
  8         241  
  1         26  
486             $gen->footer(
487 1         27 ( @{$self->_credit_elements} ? $gen->aside(map { $_->as_xml_gen } @{$self->_credit_elements}) : undef ),
  1         28  
  1         24  
488 1 50       151 ( @{$self->_footer_elements} ? map { $_->as_xml_gen } @{$self->_footer_elements} : undef ),
  1 50       150  
  1 50       30  
  1 50       25  
    50          
    50          
    50          
    50          
489             ),
490             ),
491             ),
492             );
493              
494 1         1624 return "<!doctype html>\n" . $xml;
495             }
496              
497             1;
498              
499             __END__
500              
501             =head1 AUTHOR
502              
503             Tore Aursand E<lt>toreau@gmail.comE<gt>
504              
505             =head1 COPYRIGHT
506              
507             Copyright 2016 - Tore Aursand
508              
509             =head1 LICENSE
510              
511             This library is free software; you can redistribute it and/or modify it under
512             the same terms as Perl itself.
513              
514             =cut