File Coverage

lib/App/Followme/FormatPage.pm
Criterion Covered Total %
statement 156 156 100.0
branch 38 42 90.4
condition 8 12 66.6
subroutine 25 25 100.0
pod 1 11 9.0
total 228 246 92.6


line stmt bran cond sub pod time code
1             package App::Followme::FormatPage;
2 2     2   1005 use 5.008005;
  2         32  
3 2     2   13 use strict;
  2         4  
  2         77  
4 2     2   12 use warnings;
  2         4  
  2         91  
5              
6 2     2   15 use lib '../..';
  2         5  
  2         13  
7              
8 2     2   290 use base qw(App::Followme::Module);
  2         5  
  2         675  
9              
10 2     2   16 use Digest::MD5 qw(md5_hex);
  2         5  
  2         127  
11 2     2   16 use File::Spec::Functions qw(abs2rel rel2abs splitdir catfile);
  2         4  
  2         120  
12 2     2   14 use App::Followme::FIO;
  2         3  
  2         3515  
13              
14             our $VERSION = "2.02";
15              
16             #----------------------------------------------------------------------
17             # Read the default parameter values
18              
19             sub parameters {
20 24     24 1 48 my ($pkg) = @_;
21              
22             return (
23 24         66 data_pkg => 'App::Followme::WebData',
24             );
25             }
26              
27             #----------------------------------------------------------------------
28             # Modify pages to match the most recently modified page
29              
30             sub run {
31 7     7 0 3686 my ($self, $folder) = @_;
32              
33 7         38 $self->update_folder($folder);
34 7         85 return;
35             }
36              
37             #----------------------------------------------------------------------
38             # Compute checksum for constant sections of page
39              
40             sub checksum_prototype {
41 26     26 0 90 my ($self, $prototype, $prototype_path) = @_;
42              
43 26         165 my $md5 = Digest::MD5->new;
44              
45             my $block_handler = sub {
46 78     78   216 my ($blockname, $locality, $blocktext) = @_;
47 78 100       196 $md5->add($blocktext) if exists $prototype_path->{$locality};
48 26         122 };
49              
50             my $prototype_handler = sub {
51 104     104   217 my ($blocktext) = @_;
52 104         351 $md5->add($blocktext);
53 104         186 return;
54 26         74 };
55              
56 26         86 $self->parse_blocks($prototype, $block_handler, $prototype_handler);
57 26         231 return $md5->hexdigest;
58             }
59              
60             #----------------------------------------------------------------------
61             # Get the prototype path for the current directory
62              
63             sub get_prototype_path {
64 15     15 0 122 my ($self, $filename) = @_;
65              
66 15         49 $filename = rel2abs($filename);
67 15         209 $filename = abs2rel($filename, $self->{top_directory});
68 15         893 my @path = splitdir($filename);
69 15         77 pop(@path);
70              
71 15         36 my %prototype_path = map {$_ => 1} @path;
  12         44  
72 15         50 return \%prototype_path;
73             }
74              
75             #----------------------------------------------------------------------
76             # Parse fields out of section tag
77              
78             sub parse_blockname {
79 239     239 0 607 my ($self, $str) = @_;
80              
81 239         783 my ($blockname, $in, $locality) = split(/\s+/, $str);
82              
83 239 100       466 if ($in) {
84 22 50 33     87 die "Syntax error in block ($str)"
85             unless $in eq 'in' && defined $locality;
86             } else {
87 217         284 $locality = '';
88             }
89              
90 239         529 return ($blockname, $locality);
91             }
92              
93             #----------------------------------------------------------------------
94             # This code considers the surrounding tags to be part of the block
95              
96             sub parse_blocks {
97 45     45 0 4843 my ($self, $page, $block_handler, $prototype_handler) = @_;
98              
99 45         75 my $locality;
100 45         66 my $block = '';
101 45         69 my $blockname = '';
102 45         605 my @tokens = split(/(<!--\s*(?:section|endsection)\s+.*?-->)/, $page);
103              
104 45         115 foreach my $token (@tokens) {
105 521 100       1677 if ($token =~ /^<!--\s*section\s+(.*?)-->/) {
    100          
106 121 100       280 die "Improperly nested block ($token)\n" if $blockname;
107              
108 120         222 ($blockname, $locality) = $self->parse_blockname($1);
109 120         246 $block .= $token
110              
111             } elsif ($token =~ /^<!--\s*endsection\s+(.*?)-->/) {
112 119         273 my ($endname) = $self->parse_blockname($1);
113 119 100 100     433 die "Unmatched ($token)\n"
114             if $blockname eq '' || $blockname ne $endname;
115              
116 117         190 $block .= $token;
117 117         255 $block_handler->($blockname, $locality, $block);
118              
119 116         167 $block = '';
120 116         179 $blockname = '';
121              
122             } else {
123 281 100       438 if ($blockname) {
124 120         243 $block .= $token;
125             } else {
126 161         262 $prototype_handler->($token);
127             }
128             }
129             }
130              
131 41 100       88 die "Unmatched block (<!-- section $blockname -->)\n" if $blockname;
132 40         108 return;
133             }
134              
135             #----------------------------------------------------------------------
136             # Extract named blocks from a page
137              
138             sub parse_page {
139 8     8 0 1323 my ($self, $page) = @_;
140              
141 8         15 my $blocks = {};
142             my $block_handler = sub {
143 20     20   42 my ($blockname, $locality, $blocktext) = @_;
144 20 100       44 if (exists $blocks->{$blockname}) {
145 1         11 die "Duplicate block name ($blockname)\n";
146             }
147 19         41 $blocks->{$blockname} = $blocktext;
148 19         29 return;
149 8         38 };
150              
151             my $prototype_handler = sub {
152 27     27   45 return;
153 8         29 };
154              
155 8         21 $self->parse_blocks($page, $block_handler, $prototype_handler);
156 7         33 return $blocks;
157             }
158              
159             #----------------------------------------------------------------------
160             # Determine if page matches prototype or needs to be updated
161              
162             sub unchanged_prototype {
163 13     13 0 39 my ($self, $prototype, $page, $prototype_path) = @_;
164              
165 13         42 my $prototype_checksum =
166             $self->checksum_prototype($prototype, $prototype_path);
167              
168 13         40 my $page_checksum =
169             $self->checksum_prototype($page, $prototype_path);
170              
171 13         26 my $unchanged;
172 13 100       29 if ($prototype_checksum eq $page_checksum) {
173 9         19 $unchanged = 1;
174             } else {
175 4         8 $unchanged = 0;
176             }
177              
178 13         50 return $unchanged;
179             }
180              
181             #----------------------------------------------------------------------
182             # Update file using prototype
183              
184             sub update_file {
185 13     13 0 40 my ($self, $file, $prototype, $prototype_path) = @_;
186              
187 13         38 my $page = fio_read_page($file);
188 13 50       48 return unless defined $page;
189              
190             # Check for changes before updating page
191 13 100       48 return 0 if $self->unchanged_prototype($prototype, $page, $prototype_path);
192              
193 4         12 $page = $self->update_page($page, $prototype, $prototype_path);
194              
195 4         15 my $modtime = fio_get_date($file);
196 4         18 fio_write_page($file, $page);
197 4         19 fio_set_date($file, $modtime);
198              
199 4         17 return 1;
200             }
201              
202             #----------------------------------------------------------------------
203             # Perform all updates on the directory
204              
205             sub update_folder {
206 10     10 0 31 my ($self, $folder, $prototype_file) = @_;
207              
208 10         59 my $index_file = $self->to_file($folder);
209 10         24 my ($prototype_path, $prototype);
210 10         50 my $modtime = fio_get_date($folder);
211              
212 10         70 my $files = $self->{data}->build('files_by_mdate_reversed', $index_file);
213 10         28 my $file = shift(@$files);
214              
215 10 100       25 if ($file) {
216             # The first update uses a file from the directory above
217             # as a prototype, if one is found
218              
219 7   33     72 $prototype_file ||= $self->find_prototype($folder, 1);
220              
221 7 50       23 if ($prototype_file) {
222 7         31 $prototype_path = $self->get_prototype_path($prototype_file);
223 7         23 my $prototype = fio_read_page($prototype_file);
224              
225 7         20 eval {$self->update_file($file, $prototype, $prototype_path)};
  7         33  
226 7         56 $self->check_error($@, $file);
227             }
228              
229             # Subsequent updates use the most recently modified file
230             # in the directory as the prototype
231              
232 7         16 $prototype_file = $file;
233 7         19 $prototype_path = $self->get_prototype_path($prototype_file);
234 7         40 $prototype = fio_read_page($prototype_file);
235             }
236              
237 10         30 my $changes = 0;
238 10         29 foreach my $file (@$files) {
239 6         13 my $change;
240 6         13 eval {$change = $self->update_file($file, $prototype, $prototype_path)};
  6         27  
241 6         38 $self->check_error($@, $file);
242              
243 6 100       18 last unless $change;
244 3         7 $changes += 1;
245             }
246              
247 10         42 fio_set_date($folder, $modtime);
248              
249             # Update files in subdirectory
250              
251 10 100 100     77 if ($changes || @$files == 0) {
252 7         42 my $folders = $self->{data}->build('folders', $index_file);
253              
254 7         22 foreach my $subfolder (@$folders) {
255 3         11 $self->update_folder($subfolder, $prototype_file);
256             }
257             }
258              
259 10         39 return;
260             }
261              
262             #----------------------------------------------------------------------
263             # Parse prototype and page and combine them
264              
265             sub update_page {
266 6     6 0 1526 my ($self, $page, $prototype, $prototype_path) = @_;
267 6 50       22 $prototype_path = {} unless defined $prototype_path;
268              
269 6         49 my $output = [];
270 6         20 my $blocks = $self->parse_page($page);
271              
272             my $block_handler = sub {
273 16     16   32 my ($blockname, $locality, $blocktext) = @_;
274 16 100       33 if (exists $blocks->{$blockname}) {
275 15 100       29 if (exists $prototype_path->{$locality}) {
276 5         11 push(@$output, $blocktext);
277             } else {
278 10         17 push(@$output, $blocks->{$blockname});
279             }
280 15         26 delete $blocks->{$blockname};
281             } else {
282 1         2 push(@$output, $blocktext);
283             }
284 16         28 return;
285 6         25 };
286              
287             my $prototype_handler = sub {
288 22     22   36 my ($blocktext) = @_;
289 22         38 push(@$output, $blocktext);
290 22         39 return;
291 6         18 };
292              
293 6         19 $self->parse_blocks($prototype, $block_handler, $prototype_handler);
294              
295 6 100       39 if (%$blocks) {
296 1         7 my $names = join(' ', sort keys %$blocks);
297 1         12 die "Unused blocks ($names)\n";
298             }
299              
300 5         45 return join('', @$output);
301             }
302              
303             1;
304             __END__
305              
306             =encoding utf-8
307              
308             =head1 NAME
309              
310             App::Followme::FormatPages - Modify pages in a directory to match a prototype
311              
312             =head1 SYNOPSIS
313              
314             use App::Followme::FormatPages;
315             my $formatter = App::Followme::FormatPages->new($configuration);
316             $formatter->run($directory);
317              
318             =head1 DESCRIPTION
319              
320             App::Followme::FormatPages updates the web pages in a folder to match the most
321             recently modified page. Each web page has sections that are different from other
322             pages and other sections that are the same. The sections that differ are
323             enclosed in html comments that look like
324              
325             <!-- section name-->
326             <!-- endsection name -->
327              
328             and indicate where the section begins and ends. When a page is changed, this
329             module checks the text outside of these comments. If that text has changed. the
330             other pages on the site are also changed to match the page that has changed.
331             Each page updated by substituting all its named blocks into corresponding block
332             in the changed page. The effect is that all the text outside the named blocks
333             are updated to be the same across all the web pages.
334              
335             Updates to the named block can also be made conditional by adding an "in" after
336             the section name. If the folder name after the "in" is included in the
337             prototype_path hash, then the block tags are ignored, it is as if the block does
338             not exist. The block is considered as part of the constant portion of the
339             prototype. If the folder is not in the prototype_path, the block is treated as
340             any other block and varies from page to page.
341              
342             <!-- section name in folder -->
343             <!-- endsection name -->
344              
345             Text in conditional blocks can be used for navigation or other sections of the
346             page that are constant, but not constant across the entire site.
347              
348             =head1 CONFIGURATION
349              
350             The following parameters are used from the configuration:
351              
352             =over 4
353              
354             =item data_pkg
355              
356             The name of the module that processes web files. The default value is
357             'App::Followme::WebData'.
358              
359              
360             The extension used by web pages. The default value is html
361              
362             =back
363              
364             =head1 LICENSE
365              
366             Copyright (C) Bernie Simon.
367              
368             This library is free software; you can redistribute it and/or modify
369             it under the same terms as Perl itself.
370              
371             =head1 AUTHOR
372              
373             Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
374              
375             =cut