File Coverage

lib/App/Followme/Web.pm
Criterion Covered Total %
statement 181 186 97.3
branch 80 94 85.1
condition 8 12 66.6
subroutine 19 19 100.0
pod 9 13 69.2
total 297 324 91.6


line stmt bran cond sub pod time code
1             package App::Followme::Web;
2              
3 21     21   2769 use 5.008005;
  21         101  
4 21     21   110 use strict;
  21         43  
  21         649  
5 21     21   117 use warnings;
  21         60  
  21         649  
6 21     21   612 use integer;
  21         63  
  21         121  
7 21     21   1127 use lib '../..';
  21         720  
  21         117  
8              
9 21     21   2742 use Carp;
  21         65  
  21         48649  
10              
11             require Exporter;
12             our @ISA = qw(Exporter);
13             our @EXPORT = qw(web_has_variables web_is_tag web_match_tags web_parse_sections
14             web_same_tag web_substitute_sections web_substitute_tags
15             web_parse_tag web_only_tags web_only_text web_split_at_tags
16             web_titled_sections);
17              
18             our $VERSION = "2.03";
19              
20             #----------------------------------------------------------------------
21             # Extract a list of parsed tags from a text
22              
23             sub web_extract_tags {
24 259     259 0 429 my ($text) = @_;
25              
26 259         492 my @tokens = web_split_at_tags($text);
27 259         570 return web_only_tags(@tokens);
28             }
29              
30             #----------------------------------------------------------------------
31             # Return true if any of a list of variables is present in a template
32              
33             sub web_has_variables {
34 15     15 1 3913 my ($text, @search_variables) = @_;
35              
36 15         36 my %template_variables;
37 15         243 foreach my $var ($text =~ /([\$\@]\w+)/g) {
38 163         301 $var =~ s/_by_\w+$//;
39 163         322 $template_variables{$var} = 1;
40             }
41              
42 15         49 for my $var (@search_variables) {
43 27 100       124 return 1 if $template_variables{$var};
44             }
45              
46 9         54 return 0;
47             }
48              
49             #----------------------------------------------------------------------
50             # Is the token an html tag?
51              
52             sub web_is_tag {
53 9046     9046 0 17956 my ($token) = @_;
54 9046 100       25639 return $token =~ /^<[^!]/ ? 1 : 0;
55             }
56              
57             #----------------------------------------------------------------------
58             # Call a function after a set of tags is matched
59              
60             sub web_match_tags {
61 220     220 1 2882 my ($pattern, $text, $matcher, $metadata, $global) = @_;
62              
63 220         295 my @tokens;
64 220         300 my $in = 0;
65 220         293 my $match_count = 0;
66 220         387 my @matches = web_extract_tags($pattern);
67              
68 220         420 foreach my $token (web_split_at_tags($text)) {
69 2193 100       3364 if (web_is_tag($token)) {
70 1093         1801 my $tag = web_parse_tag($token);
71 1093 100       2223 if (web_same_tag($matches[$in], $tag)) {
72 483         664 $in += 1;
73 483 100       1160 if ($in >= @matches) {
74 326         535 push(@tokens, $token);
75 326         961 $matcher->($metadata, @tokens);
76 326         486 $match_count += 1;
77 326         496 @tokens = ();
78 326         457 $in = 0;
79              
80 326 100       911 last unless $global;
81             }
82             }
83             }
84 2036 100       4182 push(@tokens, $token) if $in > 0;
85             }
86              
87 220         1044 return $match_count;
88             }
89              
90             #----------------------------------------------------------------------
91             # Extract a list of parsed tags from a set of tokens
92              
93             sub web_only_tags {
94 429     429 1 825 my (@tokens) = @_;
95              
96 429         577 my @tags;
97 429         727 foreach my $token (@tokens) {
98 631 100       1100 if (web_is_tag($token)) {
99 626         1156 push(@tags, web_parse_tag($token));
100             }
101             }
102              
103 429         1115 return @tags;
104             }
105              
106             #----------------------------------------------------------------------
107             # Parse a text string from a set of tokens
108              
109             sub web_only_text {
110 215     215 1 1292 my (@tokens) = @_;
111              
112 215         291 my @text;
113 215         392 foreach my $token (@tokens) {
114 677 100       1099 push(@text, $token) unless web_is_tag($token);
115             }
116              
117 215         571 my $text = join(' ', @text);
118 215         884 $text =~ s/\s+/ /g;
119 215         454 $text =~ s/^\s+//;
120 215         545 $text =~ s/\s+$//;
121              
122 215         674 return $text;
123             }
124              
125             #----------------------------------------------------------------------
126             # Extract sections from file, store in hash
127              
128             sub web_parse_sections {
129 57     57 1 1669 my ($text) = @_;
130              
131 57         96 my $name;
132             my %section;
133              
134             # Extract sections from input
135              
136 57         986 my @tokens = split (/(<!--\s*(?:section|endsection)\s+.*?-->)/, $text);
137              
138 57         161 foreach my $token (@tokens) {
139 717 100       2509 if ($token =~ /^<!--\s*section\s+(\w+).*?-->/) {
    100          
    100          
140 165 50       333 if (defined $name) {
141 0         0 die "Nested sections in input: $token\n";
142             }
143 165         378 $name = $1;
144              
145             } elsif ($token =~ /^<!--\s*endsection\s+(\w+).*?-->/) {
146 165 50       419 if ($name ne $1) {
147 0         0 die "Nested sections in input: $token\n";
148             }
149 165         271 undef $name;
150              
151             } elsif (defined $name) {
152 165         425 $section{$name} = $token;
153             }
154             }
155              
156 57 50       127 die "Unmatched section (<!-- section $name -->)\n" if $name;
157 57         330 return \%section;
158             }
159              
160             #----------------------------------------------------------------------
161             # Parse a web tag into attributes and their values
162              
163             sub web_parse_tag {
164 3002     3002 1 10389 my ($tag) = @_;
165 3002 50       4329 croak "Not a tag: ($tag)" unless web_is_tag($tag);
166              
167 3002         4361 my @pattern;
168 3002         3864 my $side = 0;
169 3002         4699 my @pair = (undef, undef);
170              
171 3002         10529 while ($tag =~ /(=|"[^"]*"|[^<>="\s]+)/gs) {
172 6941         12743 my $token = $1;
173              
174 6941 100       14858 if ($token eq '=') {
    100          
175 1158         1576 $side = 1;
176 1158         1538 undef $token;
177             } elsif ($token =~ /^"/) {
178 1046         2980 $token =~ s/"//g;
179             }
180              
181 6941 100       13533 if (defined $token) {
182 5783 100       9965 if (defined $pair[$side]) {
183 1623 50       2647 if (defined $pair[0]) {
184 1623         2909 push(@pattern, @pair);
185 1623         2871 @pair = (undef, undef);
186 1623         2283 $side = 0;
187             }
188             }
189              
190 5783 100       10780 $token = lc($token) if $side == 0;
191 5783         19274 $pair[$side] = $token;
192             }
193             }
194              
195 3002 50       6923 push(@pattern, @pair) if defined $pair[0];
196              
197 3002 50 33     9175 if (@pattern < 2 || defined $pattern[1]) {
198 0         0 unshift(@pattern, undef, undef);
199             }
200              
201 3002         5483 $tag = shift @pattern;
202 3002         3849 shift @pattern;
203              
204 3002         7256 my %pattern = ('_', $tag, @pattern);
205 3002         7249 return \%pattern;
206             }
207              
208             #----------------------------------------------------------------------
209             # Test if two parsed tags have the same name and attributes.
210              
211             sub web_same_tag {
212 2371     2371 0 4515 my ($match, $tag) = @_;
213              
214 2371 50       4532 croak "Match not parsed: $match" unless ref $match;
215 2371 50       3900 croak "Tag not parsed: $tag" unless ref $tag;
216              
217 2371         5175 foreach my $name (keys %$match) {
218 2723 100       5197 return 0 unless exists $tag->{$name};
219 2511         3603 my $value = $match->{$name};
220 2511 100 100     9436 return 0 if $value ne '*' && $tag->{$name} ne $value;
221             }
222              
223 599         1335 return 1;
224             }
225              
226             #----------------------------------------------------------------------
227             # Return a list of tokens, split at tag boundaries
228              
229             sub web_split_at_tags {
230 526     526 0 2300 my ($text) = @_;
231              
232 526         734 my @tokens;
233 526 100       978 if ($text) {
234 522         7335 @tokens = split(/(<!--.*?-->|<[^">]*(?:"[^"]*")*[^>]*>)/s, $text);
235 522         1090 @tokens = grep {length} @tokens;
  7385         11821  
236             }
237              
238 526         2114 return @tokens;
239             }
240              
241             #----------------------------------------------------------------------
242             # Substitue comment delimeted sections for same blocks in template
243              
244             sub web_substitute_sections {
245 64     64 1 1702 my ($text, $section) = @_;
246              
247 64         125 my $name;
248             my @output;
249              
250 64         1011 my @tokens = split (/(<!--\s*(?:section|endsection)\s+.*?-->)/, $text);
251              
252 64         179 foreach my $token (@tokens) {
253 744 100       2482 if ($token =~ /^<!--\s*section\s+(\w+).*?-->/) {
    100          
    100          
254 170 50       363 if (defined $name) {
255 0         0 die "Nested sections in template: $name\n";
256             }
257              
258 170         376 $name = $1;
259 170         306 push(@output, $token);
260              
261             } elsif ($token =~ /^\s*<!--\s*endsection\s+(\w+).*?-->/) {
262 170 50       454 if ($name ne $1) {
263 0         0 die "Nested sections in template: $name\n";
264             }
265              
266 170         250 undef $name;
267 170         290 push(@output, $token);
268              
269             } elsif (defined $name) {
270 170   66     583 $section->{$name} ||= $token;
271 170         320 push(@output, $section->{$name});
272              
273             } else {
274 234         447 push(@output, $token);
275             }
276             }
277              
278 64         514 return join('', @output);
279             }
280              
281             #----------------------------------------------------------------------
282             # Call a function after a set of tags is matched to generate substitute
283              
284             sub web_substitute_tags {
285 36     36 1 2001 my ($pattern, $text, $substituter, $output, $global) = @_;
286              
287 36         60 my @tokens;
288             my @all_tokens;
289              
290 36         46 my $in = 0;
291 36 50       79 my $match_count = $global ? 99999 : 1;
292 36         76 my @matches = web_extract_tags($pattern);
293              
294 36         74 foreach my $token (web_split_at_tags($text)) {
295 1060 100       1576 if (web_is_tag($token)) {
296 502         810 my $tag = web_parse_tag($token);
297 502 100       901 if (web_same_tag($matches[$in], $tag)) {
298 58 50       114 $in += 1 if $match_count;
299 58 100       140 if ($in >= @matches) {
300 35         59 push(@tokens, $token);
301 35         94 $token = $substituter->($output, @tokens);
302 35         63 $match_count -= 1;
303 35         60 @tokens = ();
304 35         80 $in = 0;
305             }
306             }
307             }
308              
309 1060 100       1797 if ($in > 0) {
310 46         83 push(@tokens, $token);
311             } else {
312 1014         1691 push(@all_tokens, $token);
313             }
314             }
315              
316 36 50       133 push(@all_tokens, @tokens) if @tokens;
317 36         365 return join('', @all_tokens);
318             }
319              
320             #----------------------------------------------------------------------
321             # Divide html into sections based on contents of header tags
322              
323             sub web_titled_sections {
324 3     3 1 7 my ($pattern, $text, $titler) = @_;
325              
326 3         7 my $title;
327             my @tokens;
328 3         0 my %section;
329              
330 3         6 my $in = 0;
331 3         8 my @matches = web_extract_tags($pattern);
332              
333 3         7 foreach my $token (web_split_at_tags($text)) {
334 1480 100       2200 if (web_is_tag($token)) {
335 772         1214 my $tag = web_parse_tag($token);
336 772 100       1420 if (web_same_tag($matches[$in], $tag)) {
337 56         73 $in += 1;
338 56 100       121 if ($in >= @matches) {
339 28         44 push(@tokens, $token);
340 28         66 $title = $titler->(@tokens);
341 28         55 @tokens = ();
342 28         49 undef $token;
343 28         50 $in = 0;
344             }
345             }
346             }
347              
348 1480 100       2654 if ($in > 0) {
349 56         83 push(@tokens, $token);
350 56         69 undef $title;
351 56         69 undef $token;
352             }
353              
354 1480 100 66     3778 if (defined $token && defined $title) {
355 1396 100       2628 $section{$title} = [] unless exists $section{$title};
356 1396         1676 push(@{$section{$title}}, $token);
  1396         2969  
357             }
358             }
359              
360 3         97 foreach my $title (keys %section) {
361 28         43 $section{$title} = join('', @{$section{$title}});
  28         231  
362 28         420 $section{$title} =~ s/^\s+//;
363             }
364              
365 3         21 return \%section;
366             }
367              
368             1;
369              
370             =pod
371              
372             =encoding utf-8
373              
374             =head1 NAME
375              
376             App::Followme::Web - Functions to parse html
377              
378             =head1 SYNOPSIS
379              
380             use App::Followme::Web;
381              
382             =head1 DESCRIPTION
383              
384             This module contains the subroutines followme uses to parse html. The code
385             is placed in a separate module because it is used by more than one other
386             module.
387              
388             =head1 SUBROUTINES
389              
390             =over 4
391              
392             =item $flag = web_has_variables($text, @search_variables);
393              
394             Return true if any of a list of variables is present in a template. The
395             variable names must include the sigil.
396              
397             =item $match_count = web_match_tags($pattern, $text, $matcher,
398             $metadata, $global);
399              
400             Match a tag pattern ($pattern) in a text ($text), pass the matched text to a
401             function ($matcher), which processes it and places it in a hash ($metadata).
402             Repeat this process for the entire text if the flag ($global) is set.
403              
404             =item $section = web_parse_sections($text);
405              
406             Place the text inside section tags into a hash indexed by the section names.
407              
408             =item $parsed_tag = web_parse_tag($tag);
409              
410             Parse a single html tag into a hash indexed by attribute name.
411              
412             =item $tags = web_only_tags(@tokens);
413              
414             Extract the tags from a text that has been split into tokens.
415              
416             =item $text = web_only_text(@tokens);
417              
418             Extract the text from a text that has been split into tokens.
419              
420             =item $text = web_substitute_sections($text, $section);
421              
422             Replace sections in a text by sections of the same name stored in a hash.
423              
424             =item $text = web_substitute_tags($pattern, $text, $substituter,
425             $output, $global);
426              
427             Match a tag pattern ($pattern) in a text ($text), pass the matched text to a
428             function ($substituter), which processes it and places it in a hash ($output) as
429             well as replaces the matched text. Repeat this process for the entire text if
430             the flag ($global) is set. Return the text with the substitutions.
431              
432             =item $sections = web_titled_sections($pattern, $text, $titler);
433              
434             Return a hash of sections from html, where the name of each section is derived
435             from the header tags that precede it. The title is built by calling the
436             subroutine passed in as $titler. It is passed the set of tags matched by
437             $pattern. A hash of sections that preceded by a matching set of header tags is
438             returned.
439              
440             =back
441              
442             =head1 LICENSE
443              
444             Copyright (C) Bernie Simon.
445              
446             This library is free software; you can redistribute it and/or modify
447             it under the same terms as Perl itself.
448              
449             =head1 AUTHOR
450              
451             Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
452              
453             =cut