File Coverage

lib/App/Followme/FormatPage.pm
Criterion Covered Total %
statement 159 159 100.0
branch 38 42 90.4
condition 8 12 66.6
subroutine 26 26 100.0
pod 2 12 16.6
total 233 251 92.8


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