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   196675 use strict;
  5         43  
  5         143  
4 5     5   26 use warnings;
  5         9  
  5         139  
5 5     5   25 use base qw(LUGS::Events::Parser::Filter);
  5         10  
  5         2193  
6 5     5   36 use boolean qw(true false);
  5         10  
  5         31  
7              
8 5     5   317 use Carp qw(croak);
  5         11  
  5         186  
9 5     5   4661 use DateTime ();
  5         2717869  
  5         255  
10 5     5   52 use List::MoreUtils qw(all);
  5         13  
  5         53  
11 5     5   6213 use LUGS::Events::Parser::Event ();
  5         12  
  5         138  
12 5     5   2655 use Params::Validate ':all';
  5         16321  
  5         9769  
13              
14             our $VERSION = '0.13';
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 5002 my $class = shift;
29              
30 6   33     44 my $self = bless {}, ref($class) || $class;
31 6         32 $self->_init(@_);
32              
33 6         28 $self->_fetch_content;
34 6         46 $self->_parse_content;
35              
36 6         20 return $self;
37             }
38              
39             sub _init
40             {
41 6     6   13 my $self = shift;
42 6     6   405 validate_pos(@_, { type => SCALAR, callbacks => { 'is a file' => sub { -f shift } } },
43 6         63 { type => HASHREF, optional => true });
44              
45 6         101 my ($file, $opts) = @_;
46              
47 6         40 $self->{Input} = $file;
48              
49 6 100       26 if (ref $opts eq 'HASH') {
50             my $valid_handlers = sub
51             {
52 5     5   11 my ($data) = @_;
53              
54 5 50       21 return false unless ref $data eq 'HASH';
55              
56 5         19 foreach my $tagname (keys %$data) {
57 9 50       30 return false unless ref $data->{$tagname} eq 'ARRAY';
58 9 50       16 return false unless scalar @{$data->{$tagname}};
  9         27  
59              
60 9         14 foreach my $entry (@{$data->{$tagname}}) {
  9         26  
61 10 50       28 return false unless ref $entry eq 'HASH';
62              
63 10         23 my %keys = map { $_ => true } keys %$entry;
  20         63  
64              
65 10 50       67 return false unless scalar keys %keys == 2;
66 10 50       74 return false unless all { exists $keys{$_} } qw(rewrite fields);
  20         47  
67              
68 10 50       48 return false unless ref \$entry->{rewrite} eq 'SCALAR';
69 10 50       27 return false unless ref $entry->{fields} eq 'ARRAY';
70              
71 10 50       19 return false unless scalar @{$entry->{fields}};
  10         38  
72             }
73             }
74              
75 5         16 return true;
76 5         28 };
77              
78 5         23 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   152 $valid_handlers->(shift);
90             },
91             },
92             },
93 5         45 purge_tags => {
94             type => ARRAYREF,
95             optional => true,
96             },
97             strip_text => {
98             type => ARRAYREF,
99             optional => true,
100             },
101             });
102              
103 5         223 foreach my $opt (qw(filter_html purge_tags strip_text tag_handlers)) {
104 20         72 $self->{ucfirst $opt} = $opts->{$opt};
105             }
106              
107 5   100     25 $self->{Purge_tags} ||= [];
108 5   100     64 $self->{Strip_text} ||= [];
109             }
110              
111 6 100       24 if ($self->{Filter_html}) {
112 5         63 $self->{parser} = $self->_init_parser;
113             }
114             }
115              
116             sub _fetch_content
117             {
118 6     6   11 my $self = shift;
119              
120 6 50       240 open(my $fh, '<', $self->{Input}) or croak "Cannot open `$self->{Input}': $!";
121 6         22 $self->{content} = do { local $/; <$fh> };
  6         25  
  6         260  
122 6         86 close($fh);
123             }
124              
125             sub _parse_content
126             {
127 6     6   15 my $self = shift;
128              
129 6         310 my @events = $self->{content} =~ /(^event .*? ^endevent)/gmsx;
130 6         21 my (@data, %ids);
131              
132 6         28 foreach my $event (@events) {
133 21         119 my @fields = split /\n/, $event;
134 21         41 my %fields;
135              
136 21         44 foreach my $field (@fields) {
137 203 100       592 if (my ($text) = $field =~ /^event \s+ (.+)/x) {
    100          
138 21         59 $fields{event} = $text;
139             }
140             elsif ($field =~ /^endevent \z/x) {
141 21         46 last;
142             }
143             else {
144 161         632 my ($name, $text) = $field =~ /^\s+ (\w+?) \s+ (.*)/x;
145 161 100       424 if ($self->{Filter_html}) {
146 104         711 my @html;
147 104         329 $self->_parse_html($text, \@html);
148 104 100       226 if (@html) {
149 31         113 $self->_strip_html(\@html);
150 31         51 push @{$fields{_html}->{$name}}, @html;
  31         107  
151             }
152             }
153 161         290 my $exists = exists $fields{$name};
154 161 100       489 $fields{$name} .= $exists ? " $text" : $text;
155             }
156             }
157              
158 21 100       85 if ($self->{Filter_html}) {
159 13         187 $self->_strip_text(\%fields);
160 13         67 $self->_rewrite_tags(\%fields);
161 13         111 $self->_purge_tags(\%fields);
162 13         51 $self->_decode_entities(\%fields);
163 13         69 $self->_encode_safe(\%fields);
164             }
165              
166 21         553 my ($year, $month, $day) = $fields{event} =~ /^(\d{4})(\d{2})(\d{2})$/;
167 21         106 my $dt = DateTime->new(year => $year, month => $month, day => $day);
168 21         7240 my $i = 1;
169 21         48 my %weekdays = map { $i++ => $_ } qw(Mo Di Mi Do Fr Sa So);
  147         309  
170              
171 21 50 66     182 $fields{day} ||= $1 if $day =~ /^0?(.+)$/;
172 21   66     100 $fields{weekday} ||= $weekdays{$dt->day_of_week};
173              
174 21         119 my ($event, $color) = map $fields{$_}, qw(event color);
175 21         82 my $id = $ids{$event}->{$color}++;
176 21         73 $fields{anchor} = join '_', ($event, $id, $color);
177              
178 21         137 push @data, LUGS::Events::Parser::Event->new(%fields);
179             }
180              
181 6 100       34 if ($self->{Filter_html}) {
182 5         72 $self->_eof_parser;
183             }
184              
185 6         46 $self->{data} = \@data;
186             }
187              
188             sub next_event
189             {
190 24     24 1 670 my $self = shift;
191              
192 24         70 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