File Coverage

blib/lib/LUGS/Events/Parser.pm
Criterion Covered Total %
statement 116 116 100.0
branch 29 40 72.5
condition 9 13 69.2
subroutine 17 17 100.0
pod 2 2 100.0
total 173 188 92.0


line stmt bran cond sub pod time code
1             package LUGS::Events::Parser;
2              
3 5     5   210987 use strict;
  5         73  
  5         169  
4 5     5   52 use warnings;
  5         11  
  5         205  
5 5     5   27 use base qw(LUGS::Events::Parser::Filter);
  5         9  
  5         2629  
6 5     5   53 use boolean qw(true false);
  5         13  
  5         46  
7              
8 5     5   470 use Carp qw(croak);
  5         13  
  5         227  
9 5     5   5492 use DateTime ();
  5         2693776  
  5         309  
10 5     5   58 use List::MoreUtils qw(all);
  5         10  
  5         59  
11 5     5   6848 use LUGS::Events::Parser::Event ();
  5         13  
  5         137  
12 5     5   2976 use Params::Validate ':all';
  5         15554  
  5         8216  
13              
14             our $VERSION = '0.12';
15              
16             validation_options(
17             on_fail => sub
18             {
19             my ($error) = @_;
20             chomp $error;
21             croak $error;
22             },
23             stack_skip => 2,
24             );
25              
26             sub new
27             {
28 6     6 1 4909 my $class = shift;
29              
30 6   33     46 my $self = bless {}, ref($class) || $class;
31 6         29 $self->_init(@_);
32              
33 6         25 $self->_fetch_content;
34 6         35 $self->_parse_content;
35              
36 6         24 return $self;
37             }
38              
39             sub _init
40             {
41 6     6   12 my $self = shift;
42 6     6   463 validate_pos(@_, { type => SCALAR, callbacks => { 'is a file' => sub { -f shift } } },
43 6         67 { type => HASHREF, optional => true });
44              
45 6         101 my ($file, $opts) = @_;
46              
47 6         43 $self->{Input} = $file;
48              
49 6 100       29 if (ref $opts eq 'HASH') {
50             my $valid_handlers = sub
51             {
52 5     5   17 my ($data) = @_;
53              
54 5 50       17 return false unless ref $data eq 'HASH';
55              
56 5         22 foreach my $tagname (keys %$data) {
57 9 50       28 return false unless ref $data->{$tagname} eq 'ARRAY';
58 9 50       17 return false unless scalar @{$data->{$tagname}};
  9         22  
59              
60 9         18 foreach my $entry (@{$data->{$tagname}}) {
  9         22  
61 10 50       25 return false unless ref $entry eq 'HASH';
62              
63 10         34 my %keys = map { $_ => true } keys %$entry;
  20         66  
64              
65 10 50       68 return false unless scalar keys %keys == 2;
66 10 50       100 return false unless all { exists $keys{$_} } qw(rewrite fields);
  20         54  
67              
68 10 50       46 return false unless ref \$entry->{rewrite} eq 'SCALAR';
69 10 50       87 return false unless ref $entry->{fields} eq 'ARRAY';
70              
71 10 50       16 return false unless scalar @{$entry->{fields}};
  10         54  
72             }
73             }
74              
75 5         30 return true;
76 5         28 };
77              
78 5         22 my @args = %$opts;
79             validate(@args, {
80             filter_html => {
81             # SCALARREF due to boolean.pm's implementation
82             type => BOOLEAN | SCALARREF,
83             },
84             tag_handlers => {
85             type => HASHREF,
86             callbacks => {
87             'valid data' => sub
88             {
89 5     5   226 $valid_handlers->(shift);
90             },
91             },
92             },
93 5         44 purge_tags => {
94             type => ARRAYREF,
95             optional => true,
96             },
97             strip_text => {
98             type => ARRAYREF,
99             optional => true,
100             },
101             });
102              
103 5         152 foreach my $opt (qw(filter_html purge_tags strip_text tag_handlers)) {
104 20         59 $self->{ucfirst $opt} = $opts->{$opt};
105             }
106              
107 5   100     22 $self->{Purge_tags} ||= [];
108 5   100     62 $self->{Strip_text} ||= [];
109             }
110              
111 6 100       22 if ($self->{Filter_html}) {
112 5         62 $self->{parser} = $self->_init_parser;
113             }
114             }
115              
116             sub _fetch_content
117             {
118 6     6   15 my $self = shift;
119              
120 6 50       303 open(my $fh, '<', $self->{Input}) or croak "Cannot open `$self->{Input}': $!";
121 6         23 $self->{content} = do { local $/; <$fh> };
  6         40  
  6         212  
122 6         85 close($fh);
123             }
124              
125             sub _parse_content
126             {
127 6     6   14 my $self = shift;
128              
129 6         642 my @events = $self->{content} =~ /(^event .*? ^endevent)/gmsx;
130 6         19 my (@data, %ids);
131              
132 6         18 foreach my $event (@events) {
133 21         176 my @fields = split /\n/, $event;
134 21         42 my %fields;
135              
136 21         46 foreach my $field (@fields) {
137 203 100       612 if (my ($text) = $field =~ /^event \s+ (.+)/x) {
    100          
138 21         139 $fields{event} = $text;
139             }
140             elsif ($field =~ /^endevent \z/x) {
141 21         53 last;
142             }
143             else {
144 161         690 my ($name, $text) = $field =~ /^\s+ (\w+?) \s+ (.*)/x;
145 161 100       436 if ($self->{Filter_html}) {
146 104         742 my @html;
147 104         300 $self->_parse_html($text, \@html);
148 104 100       260 if (@html) {
149 31         123 $self->_strip_html(\@html);
150 31         53 push @{$fields{_html}->{$name}}, @html;
  31         100  
151             }
152             }
153 161         284 my $exists = exists $fields{$name};
154 161 100       494 $fields{$name} .= $exists ? " $text" : $text;
155             }
156             }
157              
158 21 100       83 if ($self->{Filter_html}) {
159 13         231 $self->_strip_text(\%fields);
160 13         74 $self->_rewrite_tags(\%fields);
161 13         98 $self->_purge_tags(\%fields);
162 13         65 $self->_decode_entities(\%fields);
163 13         88 $self->_encode_safe(\%fields);
164             }
165              
166 21         569 my ($year, $month, $day) = $fields{event} =~ /^(\d{4})(\d{2})(\d{2})$/;
167 21         134 my $dt = DateTime->new(year => $year, month => $month, day => $day);
168 21         8024 my $i = 1;
169 21         55 my %weekdays = map { $i++ => $_ } qw(Mo Di Mi Do Fr Sa So);
  147         333  
170              
171 21 50 66     195 $fields{day} ||= $1 if $day =~ /^0?(.+)$/;
172 21   66     101 $fields{weekday} ||= $weekdays{$dt->day_of_week};
173              
174 21         137 my ($event, $color) = map $fields{$_}, qw(event color);
175 21         91 my $id = $ids{$event}->{$color}++;
176 21         85 $fields{anchor} = join '_', ($event, $id, $color);
177              
178 21         174 push @data, LUGS::Events::Parser::Event->new(%fields);
179             }
180              
181 6 100       81 if ($self->{Filter_html}) {
182 5         73 $self->_eof_parser;
183             }
184              
185 6         60 $self->{data} = \@data;
186             }
187              
188             sub next_event
189             {
190 24     24 1 600 my $self = shift;
191              
192 24         66 return $self->{data}->[$self->{index}++];
193             }
194              
195             1;
196             __END__
197              
198             =head1 NAME
199              
200             LUGS::Events::Parser - Event parser for the Linux User Group Switzerland
201              
202             =head1 SYNOPSIS
203              
204             use LUGS::Events::Parser;
205              
206             $parser = LUGS::Events::Parser->new($events_file);
207              
208             while ($event = $parser->next_event) {
209             $date = $event->get_event_date;
210             ...
211             }
212              
213             =head1 DESCRIPTION
214              
215             C<LUGS::Events::Parser> parses the events file of the Linux User Group
216             Switzerland (LUGS). It offers according accessor methods and may optionally
217             filter HTML markup.
218              
219             =head1 CONSTRUCTOR
220              
221             =head2 new
222              
223             Creates a new C<LUGS::Events::Parser> object.
224              
225             Without options:
226              
227             $parser = LUGS::Events::Parser->new('/path/to/events_file');
228              
229             With filtering options (example):
230              
231             $parser = LUGS::Events::Parser->new('/path/to/events_file', {
232             filter_html => 1,
233             tag_handlers => {
234             'a href' => [ {
235             rewrite => '$TEXT - $HREF',
236             fields => [ qw(location responsible) ],
237             } ],
238             },
239             purge_tags => [ qw(responsible) ],
240             strip_text => [ 'mailto:' ],
241             });
242              
243             =over 4
244              
245             =item * C<filter_html>
246              
247             Extract HTML and rewrite it. Accepts a boolean.
248              
249             =item * C<tag_handlers>
250              
251             Handlers for rewriting HTML. See L<TAG HANDLERS> for a format explanation.
252              
253             =item * C<purge_tags>
254              
255             Optionally purge all remaining tags without attribute values. Accepts an
256             array reference with field names.
257              
258             =item * C<strip_text>
259              
260             Optionally strip text from filtered content. Accepts an array reference
261             with literals.
262              
263             =back
264              
265             =head1 METHODS
266              
267             =head2 next_event
268              
269             $event = $parser->next_event;
270              
271             Returns a C<LUGS::Events::Parser::Event> object.
272              
273             =head2 get_event_date
274              
275             $date = $event->get_event_date;
276              
277             Fetch the full C<'event'> date field.
278              
279             =head2 get_event_year
280              
281             $year = $event->get_event_year;
282              
283             Fetch the event year.
284              
285             =head2 get_event_month
286              
287             $month = $event->get_event_month;
288              
289             Fetch the event month.
290              
291             =head2 get_event_day
292              
293             $day = $event->get_event_day;
294              
295             Fetch the event day.
296              
297             =head2 get_event_simple_day
298              
299             $simple_day = $event->get_event_simple_day;
300              
301             Fetch the event C<'day'> field (without zeroes).
302              
303             =head2 get_event_weekday
304              
305             $weekday = $event->get_event_weekday;
306              
307             Fetch the event C<'weekday'> field.
308              
309             =head2 get_event_time
310              
311             $time = $event->get_event_time;
312              
313             Fetch the event C<'time'> field.
314              
315             =head2 get_event_title
316              
317             $title = $event->get_event_title;
318              
319             Fetch the event C<'title'> field.
320              
321             =head2 get_event_color
322              
323             $color = $event->get_event_color;
324              
325             Fetch the event C<'color'> field.
326              
327             =head2 get_event_location
328              
329             $location = $event->get_event_location;
330              
331             Fetch the event C<'location'> field.
332              
333             =head2 get_event_responsible
334              
335             $responsible = $event->get_event_responsible;
336              
337             Fetch the event C<'responsible'> field.
338              
339             =head2 get_event_more
340              
341             $more = $event->get_event_more;
342              
343             Fetch the event C<'more'> field.
344              
345             =head2 get_event_anchor
346              
347             $anchor = $event->get_event_anchor;
348              
349             Fetch the unique event anchor.
350              
351             =head1 FILTERING AND REWRITING
352              
353             Filtering HTML markup and separating it from plaintext is optional and may
354             be enabled via the C<filter_html> option. The C<filter_html> option set on
355             its own does not suffice since no according tag handlers are defined which
356             must be provided by the C<tag_handlers> option. Remaining tags without
357             attribute values may be purged by the C<purge_tags> option. The C<strip_text>
358             option may contain literal strings to be removed from the filtered and
359             rewritten content.
360              
361             The order of processing is: HTML markup is filtered first and then being
362             rewritten by the according tag handlers. Next tags are purged if requested.
363             Then literal strings as specified are stripped from the content. Finally,
364             HTML entities are unconditionally decoded and furthermore, some field values
365             encoded to UTF-8.
366              
367             C<LUGS::Events::Parser> internally uses L<HTML::Parser> to push tags and text
368             on a stack. If tags are nested, the innermost tag will be retrieved first and
369             the outermost tag last. The top of the stack will be removed after the data
370             for each tag set has been gathered completely.
371              
372             =head1 TAG HANDLERS
373              
374             HTML markup is rewritten through the tag handlers provided within the options
375             of the constructor. The handlers of a tag group are referenced by either its
376             tagname or its tagname and an attribute name. Each handler must consist of a
377             mandatory C<rewrite> and C<fields> entry. The C<rewrite> entry defines the
378             substitute pattern for HTML markup (i.e., start tag, text and end tag) found.
379             The pattern may consist of placeholders (e.g., C<$NAME>), simple text or both.
380             It may also be empty (which has the effect of removing the markup and text
381             entirely).
382              
383             For tags which enclose text, the placeholder C<$TEXT> will represent the
384             enclosed text. If attributes are available, for example C<href>, then C<$HREF>
385             will contain the value of the C<href> attribute. Placeholders provided for
386             standalone tags will not be substituted.
387              
388             The C<fields> entry contains the field names to which rewriting applies.
389             Specifying a literal C<*> will match all field names.
390              
391             =head1 SEE ALSO
392              
393             L<http://www.lugs.ch/lugs/>
394              
395             =head1 AUTHOR
396              
397             Steven Schubiger <schubiger@cpan.org>
398              
399             =head1 LICENSE
400              
401             This program is free software; you may redistribute it and/or
402             modify it under the same terms as Perl itself.
403              
404             See L<http://dev.perl.org/licenses/>
405              
406             =cut