File Coverage

blib/lib/Blio/Node.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Blio::Node;
2              
3             # ABSTRACT: A Blio Node
4              
5 5     5   356014 use 5.010;
  5         17  
  5         179  
6 5     5   4440 use Moose;
  0            
  0            
7             use namespace::autoclean;
8             use MooseX::Types::Path::Class;
9             use Moose::Util::TypeConstraints;
10             use DateTime::Format::ISO8601;
11             use Encode;
12             use Markup::Unified;
13             use Blio::Image;
14             use XML::Atom::SimpleFeed;
15             use DateTime::Format::RFC3339;
16              
17             class_type 'DateTime';
18             coerce 'DateTime' => from 'Int' => via {
19             my $d = DateTime->from_epoch( epoch => $_ );
20             $d->set_time_zone('local');
21             return $d;
22             } => from 'Str' => via { DateTime::Format::ISO8601->parse_datetime($_) };
23              
24             has 'base_dir' => ( is => 'ro', isa => 'Path::Class::Dir', required => 1 );
25             has 'source_file' =>
26             ( is => 'ro', isa => 'Path::Class::File', required => 1, coerce => 1 );
27             has 'id' => (is => 'ro', isa=>'Str', required=>1, lazy_build=>1);
28             sub _build_id {
29             my $self = shift;
30             my $path = $self->source_file->relative($self->base_dir)->stringify;
31             $path=~s/\.txt$//;
32             return $path;
33             }
34             has 'url' => ( is => 'ro', isa => 'Str', lazy_build => 1 );
35             sub _build_url {
36             my $self = shift;
37             return $self->id.'.html';
38             }
39              
40             has 'template' => (is=>'rw',isa=>'Str',required=>1,default=>'node.tt');
41             has 'title' => ( is => 'ro', isa => 'Str', required => 1 );
42             has 'date' => (
43             is => 'rw',
44             isa => 'DateTime',
45             required => 1,
46             lazy_build => 1,
47             coerce => 1
48             );
49             sub _build_date {
50             my $self = shift;
51             my $stat = $self->source_file->stat;
52             return $stat->mtime;
53             }
54              
55             has 'language' => (is=>'ro', isa=>'Maybe[Str]');
56             has 'converter' => (is=>'ro', isa=>'Maybe[Str]');
57             has 'feed' => (is=>'ro',isa=>'Bool',default=>0);
58             has 'author' => (is=>'ro',isa=>'Str');
59             has 'paged_list' => (is=>'ro',isa=>'Int',default=>0);
60             has 'prev_next_nav' => (is=>'ro',isa=>'Int',default=>0);
61             has 'list_image' => (is=>'ro',isa=>'Str');
62              
63             has 'raw_content' => ( is => 'rw', isa => 'Str' );
64             has 'content' => ( is => 'rw', isa => 'Str', lazy_build=>1 );
65             sub _build_content {
66             my $self = shift;
67             my $converter = $self->converter;
68             my $raw_content = $self->raw_content;
69             return $raw_content unless $converter;
70              
71             if ($self->inline_images) {
72             $raw_content=~s/<bliothumb:(.*?)>/$self->call_on_image_by_name($1,'thumbnail')/ge;
73             $raw_content=~s/<blioimg:(.*?)>/$self->call_on_image_by_name($1,'url')/ge;
74              
75             $raw_content=~s/<bliothumb#(\d+)>/$self->call_on_image_by_index($1,'thumbnail')/ge;
76             $raw_content=~s/<blioimg#(\d+)>/$self->call_on_image_by_index($1,'url')/ge;
77             }
78              
79             no if $] >= 5.018, 'warnings', "experimental::smartmatch"; # TODO # why, oh, why???
80             given ($converter) {
81             when ('html') { return $raw_content }
82             when ([qw(textile markdown bbcode)]) {
83             my $o = Markup::Unified->new();
84             return $o->format($raw_content, 'textile')->formatted;
85             }
86             default {
87             my $method = 'convert_'.$converter;
88             if ($self->can($method)) {
89             return $self->$method($raw_content);
90             }
91             else {
92             return "<pre>No such converter: $converter</pre>".$raw_content;
93             }
94             }
95             }
96             }
97             has 'tags' => (
98             is => 'rw',
99             isa => 'ArrayRef',
100             default => sub { [] },
101             traits => ['Array'],
102             handles => {
103             has_tags => 'count',
104             },
105             );
106             has 'images' => (
107             is => 'rw',
108             isa => 'ArrayRef[Blio::Image]',
109             default => sub { [] },
110             traits => ['Array'],
111             handles => {
112             has_images => 'count',
113             add_image => 'push',
114             },
115             );
116             has 'inline_images' => (is=>'ro',isa=>'Bool',default=>0);
117             has 'thumbnail' => (is=>'ro',isa=>'Int');
118              
119             has 'children' => (
120             is => 'rw',
121             isa => 'ArrayRef[Blio::Node]',
122             default => sub { [] },
123             traits => ['Array'],
124             handles => {
125             has_children => 'count',
126             add_child => 'push',
127             },
128              
129             );
130             has 'parent' => ( is => 'rw', isa => 'Maybe[Blio::Node]', weak_ref => 1);
131             has 'stash' => (is=>'ro',isa=>'HashRef',default=>sub {{}});
132             has 'feed_url' => (is=>'ro',isa=>'Str',lazy_build=>1);
133             sub _build_feed_url {
134             my $self = shift;
135             return $self->id.'.xml';
136             }
137              
138             sub new_from_file {
139             my ( $class, $blio, $file ) = @_;
140             my @lines = $file->slurp(
141             chomp => 1,
142             iomode => '<:encoding(UTF-8)',
143             );
144             my ( $header, $raw_content ) = $class->parse(@lines);
145             my $tags = delete $header->{tags};
146             my $node = $class->new(
147             base_dir => $blio->source_dir,
148             language => $blio->language,
149             converter => $blio->converter,
150             source_file => $file,
151             %$header,
152             raw_content => $raw_content,
153             stash=>$header,
154             );
155              
156             $node->register_tags($blio, $tags) if $tags && $blio->tags;
157              
158             # check and add single image
159             foreach my $ext (qw(jpg jpeg png)) {
160             my $single_image = $file->basename;
161             $single_image =~ s/\.txt$/.$ext/i;
162             my $single_image_file = $file->parent->file($single_image);
163             if (-e $single_image_file) {
164             my $img = Blio::Image->new(
165             base_dir => $blio->source_dir,
166             source_file => $single_image_file,
167             );
168             $node->add_image($img);
169             }
170             }
171              
172             # check and add images dir
173             my $img_dir = $file->basename;
174             $img_dir=~s/\.txt$//;
175             $img_dir = $file->parent->subdir($img_dir.'_images');
176             if (-d $img_dir) {
177             while (my $image_file = $img_dir->next) {
178             next unless $image_file =~ /\.(jpe?g|png)$/i;
179             my $img = Blio::Image->new(
180             base_dir => $blio->source_dir,
181             source_file => $image_file,
182             );
183             $node->add_image($img);
184             }
185             }
186              
187             return $node;
188             }
189              
190             sub parse {
191             my ( $class, @lines ) = @_;
192             my %header;
193             while ( my $line = shift(@lines) ) {
194             last if $line =~ /^\s+$/;
195             last unless $line =~ /:/;
196             chomp($line);
197             $line=~s/\s+$//;
198             my ( $key, $value ) = split( /\s*:\s*/, $line, 2 );
199             $header{ lc($key) } = $value;
200             }
201             my $content = join( "\n", @lines );
202             return \%header, $content;
203             }
204              
205             sub write {
206             my ($self, $blio) = @_;
207              
208             my $tt = $blio->tt;
209             my $outfile = $blio->output_dir->file($self->url);
210             $outfile->parent->mkpath unless (-d $outfile->parent);
211              
212             $tt->process($self->template,
213             {
214             node=>$self,
215             blio=>$blio,
216             base=>$self->relative_root,
217             },
218             ,$outfile->relative($blio->output_dir)->stringify,
219             binmode => ':utf8',
220             ) || die $tt->error;
221              
222             my $utime = $self->date->epoch;
223             if ($self->has_children) {
224             my $children = $self->sorted_children;
225             my $child_utime = $children->[0]->date->epoch;
226             $utime = $child_utime if $child_utime > $utime;
227             }
228             utime($utime,$utime,$outfile->stringify);
229              
230             if ($self->has_images) {
231             foreach my $img (@{$self->images}) {
232             if ($blio->force || !-e $blio->output_dir->file($img->thumbnail)) {
233             say "\timage ".$img->url unless $blio->quiet;
234             $img->publish($blio);
235             $img->make_thumbnail($blio, $self->thumbnail);
236             }
237             }
238             }
239              
240             $self->write_feed($blio) if $self->feed;
241             }
242              
243             sub write_paged_list {
244             my ($self, $blio) = @_;
245              
246             my $list = $self->sorted_children;
247             my $items_per_page = $self->paged_list;
248             my $current_page = 1;
249             my $outfile = $blio->output_dir->file($self->url);
250             my $utime=0;
251             my @page;
252             foreach my $i (0 .. $#{$list}) {
253             if ($i>0 && $i % $items_per_page == 0) {
254             # write this page
255             $self->_write_page($blio, \@page, $current_page, $list, $outfile, $utime, $i);
256              
257             # start new page
258             my $utime=0;
259             @page=();
260             $current_page++;
261             $outfile = $blio->output_dir->file($self->id."_".$current_page.'.html');
262             }
263             push(@page, $list->[$i]);
264             my $this_utime = $list->[$i]->date->epoch;
265             $utime = $this_utime if $this_utime > $utime;
266             # write last page
267             $self->_write_page($blio, \@page, $current_page, $list, $outfile, $utime) if $i == $#{$list} ;
268             }
269              
270             $self->write_feed($blio) if $self->feed;
271             }
272              
273             sub _write_page {
274             my ($self, $blio, $page, $current_page, $list, $outfile, $utime, $i ) = @_;
275             my $tt = $blio->tt;
276             my $data = {
277             node=>$self,
278             page=>$page,
279             blio=>$blio,
280             base=>$self->relative_root,
281             };
282             $data->{prev} = sprintf("%s_%i.html",$self->id, $current_page-1) if $current_page > 1;
283             $data->{prev} = $self->url if $current_page==2;
284             $data->{next} = sprintf("%s_%i.html",$self->id, $current_page+1) if $i && $list->[$i+1];
285             $tt->process($self->template,
286             $data,
287             ,$outfile->relative($blio->output_dir)->stringify,
288             binmode => ':utf8',
289             ) || die $tt->error;
290             utime($utime,$utime,$outfile->stringify);
291             }
292              
293             sub relative_root {
294             my $self = shift;
295             my $url = $self->url;
296             my @level = $url=~m{/}g;
297            
298             return '' unless @level;
299            
300             return join('/',map { '..' } @level).'/';
301             }
302              
303             sub possible_parent_url {
304             my $self = shift;
305             my $ppurl = $self->url;
306             $ppurl =~ s{/[\-\w]+.html$}{.html};
307             return $ppurl;
308             }
309              
310             sub sorted_children {
311             my ($self, $limit) = @_;
312             my @sorted =
313             map { $_->[0] }
314             sort { $b->[1] <=> $a->[1] }
315             map { [$_ => $_->date->epoch] } @{$self->children};
316             if ($limit && $limit < @sorted) {
317             @sorted = splice(@sorted,0,$limit);
318             }
319             return \@sorted;
320             }
321              
322             sub sort_children_by {
323             my ($self, $order_by, $limit) = @_;
324             my @sorted =
325             map { $_->[0] }
326             sort { $a->[1] cmp $b->[1] }
327             map { [$_ => lc($_->$order_by)] } @{$self->children};
328             if ($limit && $limit < @sorted) {
329             @sorted = splice(@sorted,0,$limit);
330             }
331             return \@sorted;
332             }
333              
334             sub sorted_images {
335             my $self = shift;
336             my @sorted =
337             map { $_->[0] }
338             sort { $a->[1] cmp $b->[1] }
339             map { [$_ => $_->source_file->basename ] } @{$self->images};
340             return \@sorted;
341             }
342              
343             sub teaser { # TODO enable simple "--fold--" syntax
344             my ($self, $length) = @_;
345             return unless $self->raw_content;
346             $length ||= 200;
347             my $teaser;
348             if ($length =~ /^\d+$/) {
349             $teaser = $self->content;
350             $teaser =~ s{</?(.*?)>}{}g;
351             $teaser = substr($teaser,0,$length);
352             $teaser =~s/\s\S+$/ .../;
353             }
354             else {
355             my $rest;
356             ($teaser, $rest) = split($length, $self->content, 2);
357             unless ($rest) {
358             $teaser = $self->teaser(200);
359             }
360             }
361             return $teaser;
362             }
363              
364             sub write_feed {
365             my ($self, $blio) = @_;
366              
367             my $site_url = $blio->site_url;
368             die "Cannot generate Atom Feed without site_url, use --site_url to set it" unless $site_url;
369             $site_url .= '/' unless $site_url =~m{/$};
370              
371             my $children = $self->sorted_children(5);
372              
373             return unless @$children;
374             my $rfc3339 = DateTime::Format::RFC3339->new();
375              
376             my $feed = XML::Atom::SimpleFeed->new(
377             title=>$self->title || 'no title',
378             author=>$blio->site_author || $0,
379             link=>{
380             href=>$site_url.$self->feed_url,
381             rel=>'self',
382             },
383             id=>$site_url.$self->feed_url,
384             updated=>$rfc3339->format_datetime($children->[0]->date),
385             );
386              
387             foreach my $child (@$children) {
388             next unless $child->parent;
389             eval {
390             my @entry = (
391             title=>$child->title || 'no title',
392             link=>$site_url.$child->url,
393             id=>$site_url.$child->url,
394             updated=>$rfc3339->format_datetime($child->date),
395             category=>$child->parent->id,
396             summary=>($child->teaser || ' '),
397             content=>$child->content,
398             );
399             push (@entry,author => $self->author) if $self->author;
400             if ($child->has_tags) {
401             foreach my $tag (@{$child->tags}) {
402             push (@entry, category => $tag->title);
403             }
404             }
405             $feed->add_entry( @entry );
406             };
407             if ($@) {
408             say "ERR $@";
409             }
410             }
411             my $feed_file = $blio->output_dir->file($self->feed_url);
412             open(my $fh,'>:encoding(UTF-8)',$feed_file->stringify) || die "Cannot write to Atom feed file $feed_file: $!";
413             $feed->print($fh);
414             close $fh;
415              
416             my $utime = $children->[0]->date->epoch;
417             utime($utime,$utime,$feed_file->stringify);
418             }
419              
420             sub register_tags {
421             my ($self, $blio, $tags ) = @_;
422             my @tags = split(/\s*,\s*/,$tags);
423             my $tagindex = $blio->tagindex;
424             my @tagnodes;
425             foreach my $tag (@tags) {
426             my $tagid = $tag;
427             $tagid=~s/\s/_/g;
428             $tagid=~s/\W/_/g;
429             my $tagnode = $blio->nodes_by_url->{"tags/$tagid.html"};
430             unless ($tagnode) {
431             $tagnode = Blio::Node->new(
432             base_dir => $blio->source_dir,
433             source_file => $0,
434             id=>"tags/".$tagid.'.html',
435             url=>"tags/$tagid.html",
436             title=>$tag,
437             date=>DateTime->new(year=>1980),
438             content=>'',
439             );
440             $blio->nodes_by_url->{$tagnode->url} = $tagnode;
441             $tagnode->parent($tagindex);
442             $tagindex->add_child($tagnode);
443             }
444             $tagnode->add_child($self);
445             if ($self->date > $tagnode->date) {
446             $tagnode->date($self->date);
447             }
448             push(@tagnodes,$tagnode);
449             }
450             $self->tags(\@tagnodes) if @tagnodes;
451             }
452              
453             sub image_by_name {
454             my ($self, $name) = @_;
455             my @found = grep { $name eq $_->source_file->basename } @{$self->images};
456             return $found[0] if @found == 1;
457             return;
458             }
459              
460             sub call_on_image_by_name {
461             my ($self, $name, $method) = @_;
462             $method ||= 'url';
463             my $img = $self->image_by_name($name);
464             return "cannnot_resolve_image_$name" unless $img;
465             return $self->relative_root . $img->$method;
466             }
467              
468             sub image_by_index {
469             my ($self, $index) = @_;
470              
471             return $self->images->[$index - 1];
472             }
473              
474             sub call_on_image_by_index {
475             my ($self, $index, $method) = @_;
476             my $img = $self->image_by_index($index);
477             return "cannnot_resolve_image_$index" unless $img;
478             return $self->relative_root . $img->$method;
479             }
480              
481             sub primary_image {
482             my $self = shift;
483             my $img;
484             if ($self->list_image) {
485             $img = $self->image_by_name( $self->list_image);
486             }
487             $img ||= $self->sorted_images->[0];
488             return $img;
489             }
490              
491             sub prev_next_post {
492             my $self = shift;
493             return unless my $p = $self->parent;
494             return unless $p->prev_next_nav;
495             my $siblings = $p->sorted_children;
496             my $hit;
497             my $i;
498             for ($i=0;$i<@$siblings;$i++) {
499             my $this = $siblings->[$i];
500             if ($this->id eq $self->id) {
501             $hit = $i;
502             last;
503             }
504             }
505             my %sib;
506             $sib{prev} = $siblings->[$i+1] if $i < $#{$siblings} && $siblings->[$i+1];
507             $sib{next} = $siblings->[$i-1] if ($i-1 >= 0 && $siblings->[$i-1]);
508             return \%sib;
509             }
510              
511             __PACKAGE__->meta->make_immutable;
512             1;
513              
514             =pod
515              
516             =encoding UTF-8
517              
518             =head1 NAME
519              
520             Blio::Node - A Blio Node
521              
522             =head1 VERSION
523              
524             version 2.003
525              
526             =head1 AUTHOR
527              
528             Thomas Klausner <domm@cpan.org>
529              
530             =head1 COPYRIGHT AND LICENSE
531              
532             This software is copyright (c) 2013 by Thomas Klausner.
533              
534             This is free software; you can redistribute it and/or modify it under
535             the same terms as the Perl 5 programming language system itself.
536              
537             =cut
538              
539             __END__
540              
541             A Blio Node.
542              
543             See L<blio.pl> for more info.
544              
545             more docs pending...
546