File Coverage

blib/lib/WWW/Offline/Toolkit.pm
Criterion Covered Total %
statement 18 243 7.4
branch 0 100 0.0
condition 0 27 0.0
subroutine 6 30 20.0
pod 0 18 0.0
total 24 418 5.7


line stmt bran cond sub pod time code
1             package WWW::Offline::Toolkit;
2              
3 1     1   23717 use 5.010000;
  1         4  
  1         30  
4 1     1   5 use strict;
  1         1  
  1         26  
5 1     1   4 use warnings;
  1         5  
  1         31  
6 1     1   870 use Data::Dumper;
  1         12050  
  1         107  
7 1     1   2054 use Parse::RecDescent;
  1         58098  
  1         9  
8 1     1   53 use File::Find qw(finddepth);
  1         3  
  1         4494  
9              
10             our $VERSION = '0.01';
11              
12             sub new
13             {
14 0     0 0   my ($class, @args) = @_;
15 0           my $self = bless {}, $class;
16 0           return $self->init(@args);
17             }
18              
19             sub init
20             {
21 0     0 0   my ($self, %options) = @_;
22              
23 0           $self->{'DataDirectory'} = './data';
24 0           $self->{'OnlineDirectory'} = './online';
25 0           $self->{'IndexFile'} = $self->{'OnlineDirectory'}.'/index.html';
26              
27 0           $self->{'PostsDirectory'} = $self->{'OnlineDirectory'}.'/posts';
28 0           $self->{'CategoriesDirectory'} = $self->{'OnlineDirectory'}.'/categories';
29              
30 0           $self->{'MainCategoryId'} = 'cat-Main';
31 0           $self->{'CategoryPageTemplateId'} = 'tmpl-Main';
32 0           $self->{'PostTeaserTemplateId'} = 'tmpl-Teaser';
33 0           $self->{'ImageTemplateId'} = 'tmpl-Image';
34              
35 0 0         map { $self->{$_} = $options{$_} if exists $self->{$_} }
  0            
36             keys %options;
37              
38 0           $self->{'Objects'} = {};
39              
40 0           return $self;
41             }
42              
43             #-------------------------------------------------------------------------------
44             sub process
45             {
46 0     0 0   my ($self) = @_;
47            
48             #-------------------------------------------------------------------------------
49             # find data files
50              
51 0           my @Files;
52             finddepth(
53             sub {
54 0 0   0     push @Files, $File::Find::name
55             if $File::Find::name =~ /\.txt$/;
56             },
57 0           $self->{'DataDirectory'});
58              
59             #-------------------------------------------------------------------------------
60             # parse concatenated file contents
61              
62 0           my $Source = '';
63 0           foreach my $file (@Files) {
64 0           print "reading $file\n";
65 0           $Source .= read_file($file);
66             }
67              
68 0           $::RD_ERRORS = 1;
69             #$::RD_WARN = 1;
70             #$::RD_HINT = 1;
71             #$::RD_TRACE = 1;
72 0           $::RD_AUTOSTUB = 1;
73              
74 0           my $Grammar = q(
75              
76            
77              
78             file: object(s)
79             { [@{$item[2]}] }
80            
81             object: "(" type id hash ")"
82             { ['object', $item[2], $item[3], $item[4]] }
83            
84             hash: pair(s)
85             {
86             my %hash;
87             foreach my $pair (@{$item[1]}) {
88             my $value = $pair->[1];
89             $value = $value->[1] if $value->[0] eq 'value';
90             $hash{$pair->[0]} = $value;
91             }
92             \%hash;
93             }
94            
95             pair: key ":" value
96             { [$item[1], $item[3]] }
97            
98             value: object | ref | string | list
99             { $item[1] }
100            
101             ref: id
102              
103             id: "#" symbol
104             { $item[2] }
105            
106             type: symbol
107             { $item[1] }
108            
109             key: symbol
110             { $item[1] }
111            
112             symbol: /[A-Za-z0-9\_\-]+/
113             { $item[1] }
114            
115             string: "{" /[^\{\}]*/ "}"
116             { ['string', $item[2]] }
117            
118             list: "[" value(s) "]"
119             { [map { $_->[1] } @{$item[2]}] }
120              
121             );
122              
123 0           my $Parser = new Parse::RecDescent($Grammar);
124 0           my $AST = $Parser->file($Source);
125              
126             #-------------------------------------------------------------------------------
127             # create objects from AST
128              
129 0           foreach my $object (@{$AST}) {
  0            
130 0           $self->create_object($object, $self->{'Objects'});
131             }
132              
133 0           my $MainCategory = $self->{'Objects'}->{$self->{'MainCategoryId'}};
134 0           my $PostsDirectory = $self->{'PostsDirectory'};
135 0           my $CategoriesDirectory = $self->{'CategoriesDirectory'};
136 0           my $CategoryPageTemplate = $self->{'Objects'}->{$self->{'CategoryPageTemplateId'}};
137 0           my $PostTeaserTemplate = $self->{'Objects'}->{$self->{'PostTeaserTemplateId'}};
138 0           my $ImageTemplate = $self->{'Objects'}->{$self->{'ImageTemplateId'}};
139              
140             #-------------------------------------------------------------------------------
141             # check object references
142              
143 0           while ($self->has_unresolved_references()) {
144 0           foreach my $id (keys %{$self->{'Objects'}}) {
  0            
145 0           $self->resolve_object_references($id);
146             }
147             }
148              
149 0           print "building website...\n";
150              
151             #-------------------------------------------------------------------------------
152             # build index.html
153              
154 0           write_file($self->{'IndexFile'},
155             ''.
156             ''.
157             ' 158             to_filename($self->{'Objects'}->{'Home'}->{'title'}).'.html">'.
159             ''.
160             ''.
161             '');
162              
163             #-------------------------------------------------------------------------------
164             # build post pages
165              
166 0 0         unless (-d $PostsDirectory) {
167 0 0         mkdir($PostsDirectory)
168             or die "failed to create directory '$PostsDirectory': $!\n";
169             }
170              
171             $self->map_objects_of_type(
172             'post', sub {
173 0     0     my ($post) = @_;
174            
175             # add navigation to post
176 0           $post->{'nav'} =
177             $self->render_category_navigation(
178             $MainCategory, $post->{'category'});
179 0           $post->{'breadcrumb'} = $self->render_breadcrumb($MainCategory, $post->{'category'}, $post);
180            
181 0           $post->{'path'} = '../';
182            
183 0           my $outfile = $PostsDirectory.'/'.to_filename($post->{'title'}).'.html';
184 0           print "writing $outfile\n";
185 0           write_file($outfile, $self->fill_template($post->{'template'}, $post));
186 0           });
187              
188             #-------------------------------------------------------------------------------
189             # build category pages
190              
191 0 0         unless (-d $CategoriesDirectory) {
192 0 0         mkdir($CategoriesDirectory)
193             or die "failed to create directory '$CategoriesDirectory': $!\n";
194             }
195              
196             $self->map_objects_of_type(
197             'category', sub {
198 0     0     my ($cat) = @_;
199            
200             # find posts of that category
201 0           my @posts;
202             $self->map_objects_of_type(
203             'post', sub {
204 0           my ($post) = @_;
205 0 0 0       push @posts, $post
206             if $post->{'category'}->{'_id_'} eq $cat->{'_id_'} ||
207             $self->is_in_category($cat, $post->{'category'});
208 0           }, 'date');
209            
210 0           my $albums = $self->render_albums_in_category($cat);
211            
212 0           $cat->{'nav'} = $self->render_category_navigation($MainCategory, $cat);
213 0           $cat->{'breadcrumb'} = $self->render_breadcrumb($MainCategory, $cat);
214 0           $cat->{'path'} = '../';
215 0           $cat->{'content'} =
216             '

'.$cat->{'title'}.'

'.
217             # links to all posts in that category
218             (scalar @posts ?
219             '
    '.
220             join('', map {
221 0 0         $_->{'url'} = '../posts/'.to_filename($_->{'title'}).'.html';
    0          
222 0           '
  • '.$self->fill_template($PostTeaserTemplate, $_).'
  • ';
    223             } @posts).
    224             ''
    225             : '

    Nothing in this category, yet.').

    226             # photo albums
    227             (length $albums ?
    228             '

    Photo albums

    '.
    229             $albums
    230             : '');
    231            
    232 0           my $outfile = $CategoriesDirectory.'/'.to_filename($cat->{'title'}).'.html';
    233 0           print "writing $outfile\n";
    234 0           write_file($outfile, $self->fill_template($CategoryPageTemplate, $cat));
    235 0           });
    236            
    237 0           return 1;
    238             }
    239              
    240             #-------------------------------------------------------------------------------
    241             sub has_unresolved_references
    242             {
    243 0     0 0   my ($self) = @_;
    244 0           foreach my $id (keys %{$self->{'Objects'}}) {
      0            
    245 0           foreach my $key (keys %{$self->{'Objects'}->{$id}}) {
      0            
    246 0           my $value = $self->{'Objects'}->{$id}->{$key};
    247 0 0 0       return 1
    248             if ref $value eq 'HASH' && exists $value->{'_ref_'};
    249             }
    250             }
    251 0           return 0;
    252             }
    253              
    254             #-------------------------------------------------------------------------------
    255             sub resolve_object_references
    256             {
    257 0     0 0   my ($self, $id) = @_;
    258             #dmp($id);
    259 0           foreach my $key (keys %{$self->{'Objects'}->{$id}}) {
      0            
    260             #dmp(' - '.$key);
    261 0           my $value = $self->{'Objects'}->{$id}->{$key};
    262 0 0 0       if (ref $value eq 'ARRAY') {
        0          
    263             # list of objects
    264 0           foreach my $num (0..scalar(@{$value})-1) {
      0            
    265 0 0 0       if (ref $value->[$num] eq 'HASH' && exists $value->[$num]->{'_ref_'}) {
    266             #dmp(' --- '.$num);
    267             #print "$id / $key / $num\n";
    268 0           $self->_resolve_object_reference($id, $key, $num);
    269             }
    270             }
    271             }
    272             elsif (ref $value eq 'HASH' && exists $value->{'_ref_'}) {
    273             #print "$id / $key\n";
    274             # reference to object
    275 0           $self->_resolve_object_reference($id, $key);
    276             }
    277             }
    278            
    279             sub _resolve_object_reference
    280             {
    281 0     0     my ($self, $id, $key, $num) = @_;
    282 0 0         my $value = (defined $num ? $self->{'Objects'}->{$id}->{$key}->[$num] : $self->{'Objects'}->{$id}->{$key});
    283             #dmp($value);
    284 0 0         if (defined $num) {
    285 0 0         die "could not find referenced object with id '".$value->{'_ref_'}."'.\n"
    286             unless exists $self->{'Objects'}->{$value->{'_ref_'}};
    287 0           $self->{'Objects'}->{$id}->{$key}->[$num] = $self->{'Objects'}->{$value->{'_ref_'}};
    288             } else {
    289 0 0         die "could not find referenced object with id '".$value->{'_ref_'}."'.\n"
    290             unless exists $self->{'Objects'}->{$value->{'_ref_'}};
    291 0           $self->{'Objects'}->{$id}->{$key} = $self->{'Objects'}->{$value->{'_ref_'}};
    292             }
    293             }
    294             }
    295              
    296             #-------------------------------------------------------------------------------
    297             sub render_albums_in_category
    298             {
    299 0     0 0   my ($self, $cat) = @_;
    300 0           my $s = '';
    301             $self->map_objects_of_type(
    302             'album', sub {
    303 0     0     my ($album) = @_;
    304 0 0 0       if ($album->{'category'}->{'_id_'} eq $cat->{'_id_'} ||
    305             $self->is_in_category($cat, $album->{'category'})) {
    306            
    307 0           $s .= '
  • '.$self->render_album($album).'
  • ';
    308             }
    309 0           }, 'date');
    310 0 0         return (length $s ? '
      '.$s.'
    ' : '');
    311             }
    312              
    313             #-------------------------------------------------------------------------------
    314             sub render_album
    315             {
    316 0     0 0   my ($self, $album) = @_;
    317 0           my $s = '';
    318             # find images in album
    319 0           my $first = 1;
    320 0           $album->{'firstimage'} = '';
    321 0           $album->{'restimages'} = '';
    322 0           foreach my $img (@{$album->{'images'}}) {
      0            
    323 0           $img->{'path'} = '../';
    324 0           $img->{'albumname'} = '['.$album->{'title'}.']';
    325 0 0         if ($first) {
    326 0           $album->{'thumbnail'}->{'path'} = '../';
    327 0           my $first = {
    328             'path' => $img->{'path'},
    329             'file' => $img->{'file'},
    330             'albumname' => '['.$album->{'title'}.']',
    331             'title' => $self->fill_template($self->{'Objects'}->{'tmpl-Image'}, $album->{'thumbnail'}),
    332             'description' => $album->{'description'},
    333             'date' => $album->{'date'},
    334             };
    335 0           $album->{'firstimage'} = $self->fill_template($self->{'Objects'}->{'tmpl-AlbumImage'}, $first);
    336             } else {
    337 0           $album->{'restimages'} .= $self->fill_template($self->{'Objects'}->{'tmpl-AlbumImageNoName'}, $img);
    338             }
    339 0           $first = 0;
    340             }
    341 0           $s .= $self->fill_template($self->{'Objects'}->{'tmpl-Album'}, $album);
    342 0           return $s;
    343             }
    344              
    345             #-------------------------------------------------------------------------------
    346             sub map_objects_of_type
    347             {
    348 0     0 0   my ($self, $type, $function, $order_by) = @_;
    349 0           foreach my $id
      0            
    350             (reverse
    351 0 0 0       map { $_->{'_id_'} }
    352 0           sort { (defined $order_by && defined $a->{$order_by} && defined $b->{$order_by} ?
    353             ($a->{$order_by} cmp $b->{$order_by}) : 0) }
    354             values %{$self->{'Objects'}}) {
    355            
    356 0           my $object = $self->{'Objects'}->{$id};
    357 0 0         if ($object->{'_type_'} eq $type) {
    358 0           $function->($object);
    359             }
    360             }
    361             }
    362              
    363             #-------------------------------------------------------------------------------
    364             sub render_breadcrumb
    365             {
    366 0     0 0   my ($self, $top_category, $current_category, $post) = @_;
    367            
    368 0           my ($crumbs, $last_link) = $self->_render_breadcrumb($top_category, $current_category);
    369 0           my $home_link = '../posts/'.to_filename($self->{'Objects'}->{'Home'}->{'title'}).'.html';
    370 0 0         my $post_link = (defined $post ? '../posts/'.to_filename($post->{'title'}).'.html' : '');
    371 0 0 0       my $s =
        0          
    372             '
      '.
    373             '
  • You are here:
  • '.
    374             ($home_link ne $last_link ?
    375             '
  • '.
  • 376             $self->{'Objects'}->{'Home'}->{'title'}.
    377             '' : '').
    378             $crumbs.
    379             (defined $post && $post_link ne $last_link ?
    380             '
  • '.
  • 381             $post->{'title'}.
    382             '' : '').
    383             '';
    384            
    385             sub _render_breadcrumb
    386             {
    387 0     0     my ($self, $top_category, $current_category) = @_;
    388            
    389 0           my $s = '';
    390 0           my $last_link = '';
    391 0 0         if (exists $top_category->{'subcategories'}) {
    392 0           my @subs = @{$top_category->{'subcategories'}};
      0            
    393 0           foreach my $item (@subs) {
    394 0 0 0       if ($self->is_in_category($item, $current_category) ||
    395             $item->{'_id_'} eq $current_category->{'_id_'}) {
    396            
    397 0 0         $last_link =
    398             (exists $item->{'targetpost'} ?
    399             '../posts/'.to_filename($item->{'targetpost'}->{'title'}):
    400             '../categories/'.to_filename($item->{'title'})).'.html';
    401 0           $s .=
    402             '
  • '.
  • 403             ''.
    404             $item->{'title'}.
    405             ' '.
    406             '';
    407             }
    408             }
    409             }
    410 0           return ($s, $last_link);
    411             }
    412             }
    413              
    414             #-------------------------------------------------------------------------------
    415             sub render_category_navigation
    416             {
    417 0     0 0   my ($self, $top_category, $current_category) = @_;
    418             #dmp($top_category);
    419 0           my $s = '';
    420 0 0         if (exists $top_category->{'subcategories'}) {
    421 0           my @subs = @{$top_category->{'subcategories'}};
      0            
    422 0 0         $s = (scalar @subs ? '
      ' : '');
    423 0           foreach my $item (@subs) {
    424             #dmp($item);
    425 0   0       my $current =
    426             $self->is_in_category($item, $current_category) ||
    427             $item->{'_id_'} eq $current_category->{'_id_'};
    428             #print $item->{'_id_'}." ($current)\n";
    429 0 0         $s .=
        0          
    430             '
  • '.
  • 431             ' 432             (exists $item->{'targetpost'} ?
    433             '../posts/'.to_filename($item->{'targetpost'}->{'title'}):
    434             '../categories/'.to_filename($item->{'title'})).
    435             '.html">'.
    436             $item->{'title'}.
    437             ' '.
    438             $self->render_category_navigation($self->{'Objects'}->{$item->{'_id_'}}, $current_category).
    439             '';
    440             }
    441 0 0         $s .= (scalar @subs ? '' : '');
    442             }
    443             #dmp($s);
    444 0           return $s;
    445             }
    446              
    447             #-------------------------------------------------------------------------------
    448             sub is_in_category
    449             {
    450 0     0 0   my ($self, $cat, $current_cat) = @_;
    451 0 0         if (exists $cat->{'subcategories'}) {
    452             # check subcats
    453 0           return scalar(grep { $self->is_in_category($_, $current_cat) } @{$cat->{'subcategories'}});
      0            
      0            
    454             }
    455             else {
    456 0 0         if ($cat->{'_id_'} eq $current_cat->{'_id_'}) {
    457 0           return 1;
    458             } else {
    459 0           return 0;
    460             }
    461             }
    462             }
    463              
    464             #-------------------------------------------------------------------------------
    465             sub to_filename
    466             {
    467 0     0 0   my ($s) = @_;
    468 0           $s =~ s/[\n\r]/ /g;
    469 0           $s =~ s/[\s\t]+/ /g;
    470 0           $s =~ s/\s/-/g;
    471 0           $s =~ s/[^a-zA-Z0-9\-\.\_]//g;
    472 0           return $s;
    473             }
    474              
    475             #-------------------------------------------------------------------------------
    476             sub dmp
    477             {
    478 0     0 0   print Dumper(@_);
    479             }
    480              
    481             sub render_sound
    482             {
    483 0     0 0   my ($self, $sound) = @_;
    484             return
    485 0 0         '
    '.
    486             '
    487             'codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0" '.
    488             'width="165" height="38" id="niftyPlayer1" align="">'.
    489             ''.
    490             ''.
    491             ''.
    492             '
    493             'quality=high bgcolor=#FFFFFF width="165" height="38" '.
    494             'name="niftyPlayer1" align="" type="application/x-shockwave-flash" '.
    495             'pluginspage="http://www.macromedia.com/go/getflashplayer">'.
    496             ''.
    497             ''.
    498             '

    '.$sound->{'title'}.' by '.

    499             (length $sound->{'artist'} ? ''.$sound->{'artist'}.'' : 'unknown').'

    '.
    500             '';
    501             }
    502              
    503             #-------------------------------------------------------------------------------
    504             sub fill_template
    505             {
    506 0     0 0   my ($self, $tmpl_object, $data_object) = @_;
    507 0           my $s = $tmpl_object->{'content'};
    508            
    509 0           foreach my $key (keys %{$data_object}) {
      0            
    510 0           my $value = $data_object->{$key};
    511 0 0         if (!ref $value) {
    512 0           my $k = quotemeta $key;
    513 0           $s =~ s/\[$k\]/$value/g;
    514             }
    515             }
    516            
    517             # replace embedded objects
    518 0           while ($s =~ /\[\#([a-zA-Z0-9\.\-\_]+)\]/) {
    519 0           my $id = $1;
    520 0 0         if (exists $self->{'Objects'}->{$id}) {
    521 0           my $object = $self->{'Objects'}->{$id};
    522 0           my $value = '';
    523 0 0         if ($object->{'_type_'} eq 'album') {
        0          
        0          
        0          
        0          
    524 0           $value = $self->render_album($object);
    525             }
    526             elsif ($object->{'_type_'} eq 'category') {
    527 0           $value = ''.$object->{'title'}.'';
    528             }
    529             elsif ($object->{'_type_'} eq 'post') {
    530 0           $value = ''.$object->{'title'}.'';
    531             }
    532             elsif ($object->{'_type_'} eq 'image') {
    533 0           $value = $self->fill_template($self->{'Objects'}->{'tmpl-Image'}, $object);
    534             }
    535             elsif ($object->{'_type_'} eq 'sound') {
    536 0           $value = $self->render_sound($object);
    537             }
    538 0           $s =~ s/\[\#$id\]/$value/g;
    539             }
    540             }
    541            
    542             # replace empty undefined placeholders with empty string
    543 0           $s =~ s/\[\#?[a-zA-Z0-9\.\-\_]+\]//g;
    544 0           return $s;
    545             }
    546              
    547             #-------------------------------------------------------------------------------
    548             sub create_object
    549             {
    550 0     0 0   my ($self, $astobj, $objects) = @_;
    551 0 0         if (ref $astobj->[0] eq 'ARRAY') {
    552             # list of objects
    553 0           return [ map { $self->create_object($_, $objects) } @{$astobj} ];
      0            
      0            
    554             }
    555             else {
    556             # single object
    557 0           my ($asttype, @parts) = @{$astobj};
      0            
    558            
    559 0 0         if ($asttype eq 'object') {
        0          
        0          
    560 0           my ($objtype, $id, $hash) = @parts;
    561 0           foreach my $key (keys %{$hash}) {
      0            
    562 0           $hash->{$key} = $self->create_object($hash->{$key}, $objects);
    563             }
    564 0 0         die "cannot redefine object with id '$id'.\n"
    565             if exists $objects->{$id};
    566 0           $hash->{'_type_'} = $objtype;
    567 0           $hash->{'_id_'} = $id;
    568 0           $objects->{$id} = $hash;
    569 0           return $objects->{$id};
    570             }
    571             elsif ($asttype eq 'string') {
    572 0           return $astobj->[1];
    573             }
    574             elsif ($asttype eq 'ref') {
    575 0           return {'_ref_' => $astobj->[1]};
    576             }
    577             }
    578             }
    579              
    580             #-------------------------------------------------------------------------------
    581             sub read_file
    582             {
    583 0     0 0   my ($filename) = @_;
    584 0 0         open(FILE, "<$filename") || die "failed to read file '$filename': $!\n";
    585 0           my $content = join '', ;
    586 0           close FILE;
    587 0           return $content;
    588             }
    589              
    590             #-------------------------------------------------------------------------------
    591             sub write_file
    592             {
    593 0     0 0   my ($filename, $string) = @_;
    594 0 0         open(FILE, ">$filename") || die "failed to write to file '$filename': $!\n";
    595 0           print FILE $string;
    596 0           close FILE;
    597             }
    598              
    599             1;
    600             __END__