File Coverage

blib/lib/iCal/Parser/SAX.pm
Criterion Covered Total %
statement 25 188 13.3
branch 1 68 1.4
condition 0 12 0.0
subroutine 7 25 28.0
pod 3 13 23.0
total 36 306 11.7


line stmt bran cond sub pod time code
1             #$Id: SAX.pm 505 2008-06-27 22:53:18Z rick $
2             package iCal::Parser::SAX;
3 1     1   24169 use strict;
  1         3  
  1         41  
4              
5 1     1   5 use base qw(XML::SAX::Base);
  1         1  
  1         1410  
6 1     1   33496 use iCal::Parser;
  1         303334  
  1         45  
7 1     1   8 use IO::File;
  1         2  
  1         115  
8 1     1   5 use IO::String;
  1         2  
  1         17  
9 1     1   4 use DateTime;
  1         2  
  1         2219  
10              
11             # Get version from subversion url of tag or branch.
12             our $VERSION= do {(q$URL: svn+ssh://xpc/var/lib/svn/rick/perl/ical/iCal-Parser-SAX/tags/1.09/lib/iCal/Parser/SAX.pm $=~ m$.*/(?:tags|branches)/([^/ ]+)$)[0]||'0.1'};
13              
14             our %NAMES=('X-WR-RELCALID'=>'id', 'X-WR-CALNAME'=>'name',
15             'X-WR-CALDESC'=>'description');
16             sub new {
17 1     1 1 13 my($class,%options)=@_;
18 1         2 my $handler=delete $options{Handler};
19 1 50       10 my $self=XML::SAX::Base->new($handler ? (Handler=>$handler) : ());
20 1         64 $self=bless $self,$class;
21 1         7 $self->{no_escape}=delete $options{no_escape};
22 1         7 $self->{_calparser}=iCal::Parser->new(%options);
23 1         11104 return $self;
24             }
25             sub _parse_characterstream {
26 0     0     shift->_parse_fh(@_);
27             }
28             sub _parse_bytestream {
29 0     0     shift->_parse_fh(@_);
30             }
31             sub _parse_systemid {
32 0     0     my ($self, $sysid, $options) = @_;
33 0           $self->_parse_fh(__systemid_to_fh($sysid));
34             }
35             sub _parse_string {
36 0     0     my ($self, $str, $options) = @_;
37              
38 0           $self->_parse_fh(IO::String->new($str));
39             }
40             sub _parse_fh {
41 0     0     my($self,$fh,$options)=@_;
42              
43 0           return $self->parse_hash($self->{_calparser}->parse($fh));
44             }
45             sub parse_uris {
46 0     0 1   my $self=shift;
47              
48 0           foreach my $uri (@_) {
49 0           $self->{_calparser}->parse(__systemid_to_fh($uri));
50             }
51 0           return $self->parse_hash($self->{_calparser}->calendar);
52             }
53             sub __systemid_to_fh {
54 0     0     my $sysid=shift;
55 0 0         if($sysid =~ m{^(http|ftp|https)://}) {
56 0           eval {require LWP::UserAgent;};
  0            
57 0 0         die "LWP required for $sysid\n" if $@;
58 0           my $req=HTTP::Request->new(GET => $sysid);
59 0           my $ua=LWP::UserAgent->new;
60 0           $ua->agent(__PACKAGE__);
61 0           my $res=$ua->request($req);
62 0 0         unless($res->is_success) {
63 0           die "Can't read $sysid\n";
64             }
65 0           return IO::String->new($res->content);
66             } else {
67 0 0         return IO::File->new($sysid,'r') or die "Can't open $sysid, $!\n";
68             }
69             }
70             sub parse_hash {
71 0     0 1   my($self,$hash)=@_;
72              
73 0           $self->SUPER::start_document;
74 0           $self->start('ical');
75 0           foreach my $cal (@{ $hash->{cals} }) {
  0            
76 0   0       $self->start('calendar',{ map {
77 0           ($NAMES{$_}||lc $_)=>$cal->{$_}
78             } keys %$cal });
79 0           $self->end('calendar');
80             }
81 0           $self->process_events($hash);
82              
83 0 0         if(scalar @{$hash->{todos}}) {
  0            
84 0           $self->start('todos');
85 0           map {$self->process_component($_,'todo')} @{ $hash->{todos} };
  0            
  0            
86 0           $self->end('todos');
87             }
88 0           $self->end('ical');
89 0           $self->SUPER::end_document;
90             }
91             sub process_events {
92 0     0 0   my($self,$hash)=@_;
93 0           my $events=$hash->{events};
94 0 0         return unless $events;
95 0           my $cals=$hash->{cals};
96              
97 0           $self->start('events');
98 0           my @years=sort { $a <=> $b } keys %$events;
  0            
99 0           foreach my $y (@years) {
100 0           $self->start('year',{year=>$y});
101 0           my $year=$events->{$y};
102             #fill in missing months from start->end
103 0           my @months=sort { $a <=> $b } keys %$year;
  0            
104 0           my $sm= $months[0];
105 0           my $se= $months[-1];
106              
107 0           foreach my $m ($sm .. $se) {
108 0           my $month=$year->{$m};
109 0           my $d1=DateTime->new(year=>$y,month=>$m,day=>1);
110 0 0         warn $d1->ymd, " ---\n" if $self->{debug};
111 0           $self->start('month', {month=>$m});
112 0           my $week=$d1->week_number;
113 0           $self->start('week',{week=>$week});
114             #pad beggining of week
115 0           my $dow=$d1->day_of_week;
116 0           for($d1->subtract(days=>$d1->day_of_week-1);$d1->day_of_week!=$dow;
117             $d1->add(days=>1)) {
118 0           $self->process_day($d1,$self->day($d1,$events));
119             }
120 0           for(;$d1->month == $m;$d1->add(days=>1)) {
121 0 0         if($d1->week != $week) {
122 0           $self->end('week',{week=>$week});
123 0           $week=$d1->week;
124 0           $self->start('week',{week=>$week});
125             }
126 0           $self->process_day($d1,$self->day($d1,$events));
127             }
128             #pad end of month
129 0           for(;$d1->day_of_week != 1;$d1->add(days=>1)) {
130 0           $self->process_day($d1,$self->day($d1,$events));
131             }
132 0           $self->end('week');
133 0           $self->end('month');
134             }
135 0           $self->end('year');
136             }
137 0           $self->end('events');
138             }
139             sub day {
140 0     0 0   my($self,$d,$events)=@_;
141 0           my($yr,$mo);
142 0 0         return unless $yr=$events->{$d->year};
143 0 0         return unless $mo=$yr->{$d->month};
144 0           return $mo->{$d->day};
145             }
146             sub process_day {
147 0     0 0   my($self,$d,$day)=@_;
148             #warn $d->ymd,"\n" if $self->{debug};
149              
150             # figure out max# conflicting appointments. and output in xml
151             # makes html generation of weekly/daily calendar easier
152 0           my @events=();
153 0           my $conflict=0;
154 0 0         if($day) {
155 0           @events=sort by_type_time values %$day;
156 0           my @a=(); #event span
157 0           foreach my $e (@events) {
158 0 0         if($e->{allday}) {
159 0           push @a,undef;
160 0           next;
161             }
162             #if an event ends at e.g., 9am and another starts
163             #at 9, intersect will generate an overlap.
164             #so, subtract 1 sec from the end of each event
165             ## unless start == end
166             ## note start > end is an error!
167 0 0         my $end=$e->{DTSTART}->compare($e->{DTEND}) < 0
168             ? $e->{DTEND}->clone->subtract(seconds=>1) : $e->{DTEND};
169 0           push @a, DateTime::Span->from_datetimes
170             (start=>$e->{DTSTART}, end=>$end);
171             }
172 0           my @overlap=(0);
173             # each conflict adds one to the count of conflicts for the event
174             # it conflicts with
175 0           foreach my $i (1..$#a) {
176 0           my $span=$a[$i];
177 0           $overlap[$i]=0;
178 0 0         next unless $span;
179 0           foreach my $j (0..$i-1) {
180 0 0         next unless $a[$j];
181 0 0         $overlap[$i]=$overlap[$j]+1 if $span->intersects($a[$j]);
182             }
183 0 0         $events[$i]->{'conflict-number'}=$overlap[$i] if $overlap[$i];
184             }
185 0 0         map { $conflict = $_ if $_ > $conflict } @overlap;
  0            
186             }
187 0 0         $self->start('day',{date=>$d->ymd,
188             $conflict ? (conflict=>$conflict) : ()});
189 0           map {$self->process_component($_,'event')} @events;
  0            
190 0           $self->end('day');
191             }
192             sub by_type_time { # For sorting lists of events
193             # Two events on the same day? All day events come first
194 0 0 0 0 0   return -1 if $a->{allday} && !$b->{allday};
195 0 0 0       return 1 if $b->{allday} && !$a->{allday};
196              
197             # If they're both all day events, sort by summary text
198 0 0 0       return $a->{SUMMARY} cmp $b->{SUMMARY} if $a->{allday} && $b->{allday};
199              
200             # Otherwise, sort by start time
201 0           return $a->{DTSTART} <=> $b->{DTSTART};
202             }
203             sub process_component {
204 0     0 0   my($self,$ee,$type)=@_;
205 0           my %attrs=();
206             # pull out attributes before generic processing
207             # of key/value pairs into elements
208             #clone in case event processed more than once
209 0           my %e=%$ee;
210 0 0         $attrs{uid}=delete $e{UID} if $e{UID};
211 0 0         $attrs{idref}=delete $e{idref} if $e{idref};
212 0 0         $attrs{'all-day'}=delete $e{allday} if $e{allday};
213             # used in xslt stylesheet to figure out which
214             # overlapping event this is
215 0 0         $attrs{'conflict-number'}=delete $e{'conflict-number'}
216             if $e{'conflict-number'};
217              
218 0           $self->start($type,%attrs);
219 0           while(my($k,$v)=each(%e)) {
220 0 0         if(ref $v eq 'ARRAY') {
    0          
221 0           my $list=$k . 's';
222 0           $self->start($list,count=>scalar @$v);
223 0           map {$self->process_component($_,$k)} @$v;
  0            
224 0           $self->end($list);
225             } elsif(ref $v eq 'HASH') {
226 0           $self->process_component($v,$k);
227             } else {
228 0           $self->text_element($k,$v);
229             }
230             }
231 0           $self->end($type);
232             }
233             sub start {
234 0     0 0   my $self=shift;
235 0           $self->SUPER::start_element($self->make_element(@_));
236             }
237             sub end {
238 0     0 0   my $self=shift;
239 0           $self->SUPER::end_element($self->make_element(@_));
240             }
241             sub make_element {
242 0     0 0   my $self=shift;
243 0           my $n=lc shift;
244 0 0         my %a=ref $_[0] ? %{$_[0]} : @_;
  0            
245 0           my %h=(Name=>"$n");
246 0 0         return \%h unless %a;
247 0           while(my($k,$v) = each %a) {
248 0           $h{Attributes}->{"{}$k"} = {Name=>$k, Value=>escape($v,$self->{no_escape})};
249             }
250 0           return \%h;
251             }
252             sub escape {
253 0     0 0   my $text=shift;
254 0           my $no_escape=shift;
255 0 0         return '' unless $text;
256 0 0         unless($no_escape) {
257 0           $text=~s/&/\&/go;
258 0           $text=~s/"/\"/go;
259 0           $text=~s/'/'/go;
260             }
261 0           $text=~s/
262 0           $text=~s/\\n/ /go;
263 0           $text=~s/\\//go;
264 0           return $text;
265             }
266             sub text_element {
267 0     0 0   my($self, $n, $v, %a)=@_;
268 0           $self->start($n, %a);
269 0 0         if($v) {
270 0           my $text=escape($v,$self->{no_escape});
271 0           $self->SUPER::characters({Data=>$text});
272             }
273 0           $self->end($n);
274             }
275             1;
276             __END__